From 069466988ff0c07e4b698c9fcc04ac14dac3f26d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 Nov 2017 09:00:45 -0700 Subject: [PATCH 001/148] Bump to 2.6.0 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174467964 --- RELEASENOTES.md | 50 +++++++++++++++++++ constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 +- demos/ima/src/main/AndroidManifest.xml | 4 +- demos/main/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +-- 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90b3d15e08..42bf526de0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,55 @@ # Release notes # +### r2.6.0 ### + +* New `Player.DefaultEventListener` abstract class can be extended to avoid + having to implement all methods defined by `Player.EventListener`. +* Added a reason to `EventListener.onPositionDiscontinuity` + ([#3252](https://github.com/google/ExoPlayer/issues/3252)). +* New `setShuffleModeEnabled` method for enabling shuffled playback. +* Support for `Renderer`s that don't consume any media + ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` + ([#3362](https://github.com/google/ExoPlayer/issues/3362)). +* Fix playbacks involving looping, concatenation and ads getting stuck when + media contains tracks with uneven durations + ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Better playback experience when the video decoder cannot keep up, by skipping + to key-frames. This is particularly relevant for variable speed playbacks. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Audio: New `AudioSink` interface allows customization of audio output path. +* Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming + and progressive streams. +* Track selection: + * Fixed adaptive track selection logic for live playbacks + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). + * Added ability to select the lowest bitrate tracks. +* HLS: + * Support for Widevine protected FMP4 variants. +* DRM: + * Improved compatibility with ClearKey content + ([#3138](https://github.com/google/ExoPlayer/issues/3138)). + * Support multiple PSSH boxes of the same type. + * Retry initial provisioning and key requests if they fail + * Fix incorrect parsing of non-CENC sinf boxes. +* IMA extension: + * Expose `AdsLoader` via getter + ([#3322](https://github.com/google/ExoPlayer/issues/3322)). + * Handle `setPlayWhenReady` calls during ad playbacks + ([#3303](https://github.com/google/ExoPlayer/issues/3303)). + * Ignore seeks if an ad is playing + ([#3309](https://github.com/google/ExoPlayer/issues/3309)). +* UI: + * Allow specifying a `Drawable` for the `TimeBar` scrubber + ([#3337](https://github.com/google/ExoPlayer/issues/3337)). + * Allow multiple listeners on `TimeBar` + ([#3406](https://github.com/google/ExoPlayer/issues/3406)). +* New Leanback extension: Simplifies binding Exoplayer to Leanback UI + components. +* New Cast extension: Simplifies toggling between local and Cast playbacks. +* Unit tests moved to Robolectric. +* Misc bugfixes. + ### r2.5.4 ### * Remove unnecessary media playlist fetches during playback of live HLS streams. diff --git a/constants.gradle b/constants.gradle index c2209cbbe6..644d47b8aa 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.5.4' + releaseVersion = 'r2.6.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index cd2b51513c..11f8e39b53 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2600" + android:versionName="2.6.0"> diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5c6db02417..5252d2feeb 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2600" + android:versionName="2.6.0"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index f70d6152e8..d041e24d80 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2600" + android:versionName="2.6.0"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 62ee8c4873..f13a7de0ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.4"; + public static final String VERSION = "2.6.0"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005004; + public static final int VERSION_INT = 2006000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From e4a0e977cd06b945d4faf149908dbeb5aa4e0a55 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 3 Nov 2017 16:23:21 +0000 Subject: [PATCH 002/148] Remove DownloadManager test --- .../offline/DownloadManagerTest.java | 432 ------------------ 1 file changed, 432 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java deleted file mode 100644 index 791b4a9d35..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ /dev/null @@ -1,432 +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.os.ConditionVariable; -import android.support.annotation.Nullable; -import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask; -import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask.State; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -/** Tests {@link DownloadManager}. */ -@ClosedSource(reason = "Not ready yet") -public class DownloadManagerTest extends InstrumentationTestCase { - - /* Used to check if condition becomes true in this time interval. */ - private static final int ASSERT_TRUE_TIMEOUT = 1000; - /* Used to check if condition stays false for this time interval. */ - private static final int ASSERT_FALSE_TIME = 1000; - - private DownloadManager downloadManager; - private File actionFile; - private TestDownloadListener testDownloadListener; - - @Override - public void setUp() throws Exception { - super.setUp(); - setUpMockito(this); - - actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); - downloadManager = new DownloadManager( - new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY), - 100, actionFile.getAbsolutePath()); - - testDownloadListener = new TestDownloadListener(); - downloadManager.addListener(testDownloadListener); - } - - @Override - public void tearDown() throws Exception { - downloadManager.release(); - actionFile.delete(); - super.tearDown(); - } - - public void testDownloadActionRuns() throws Throwable { - doTestActionRuns(createDownloadAction("media 1")); - } - - public void testRemoveActionRuns() throws Throwable { - doTestActionRuns(createRemoveAction("media 1")); - } - - public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createDownloadAction("media 2")); - } - - public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createRemoveAction("media 2")); - } - - public void testSameMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createDownloadAction("media 1")); - } - - public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { - doTestActionsRunSequentially(createDownloadAction("media 1"), - createRemoveAction("media 1")); - } - - public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), - createDownloadAction("media 1")); - } - - public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), - createRemoveAction("media 1")); - } - - public void testSameMediaMultipleActions() throws Throwable { - FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction("media 1"); - FakeDownloadAction downloadAction3 = createDownloadAction("media 1"); - FakeDownloadAction removeAction2 = createRemoveAction("media 1"); - - // Two download actions run in parallel. - downloadAction1.post().assertStarted(); - downloadAction2.post().assertStarted(); - // removeAction1 is added. It interrupts the two download actions' threads but they are - // configured to ignore it so removeAction1 doesn't start. - removeAction1.post().assertDoesNotStart(); - - // downloadAction2 finishes but it isn't enough to start removeAction1. - downloadAction2.finish().assertCancelled(); - removeAction1.assertDoesNotStart(); - // downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. - downloadAction3.post().assertDoesNotStart(); - - // When downloadAction1 finishes, removeAction1 starts. - downloadAction1.finish().assertCancelled(); - removeAction1.assertStarted(); - // downloadAction3 still waits removeAction1 - downloadAction3.assertDoesNotStart(); - - // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 - // starts immediately. - removeAction2.post().assertStarted().finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { - FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction2 = createRemoveAction("media 1"); - FakeDownloadAction removeAction3 = createRemoveAction("media 1"); - - removeAction1.post().assertStarted(); - removeAction2.post().assertDoesNotStart(); - removeAction3.post().assertDoesNotStart(); - - removeAction2.assertCancelled(); - - removeAction1.finish().assertCancelled(); - removeAction3.assertStarted().finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 1"); - - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); - - removeAction.finish().assertEnded(); - downloadAction1.assertStarted(); - downloadAction2.assertStarted(); - downloadAction1.finish().assertEnded(); - downloadAction2.finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { - FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); - FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); - FakeDownloadAction downloadAction2 = createDownloadAction("media 2"); - - removeAction.post().assertStarted(); - downloadAction1.post().assertDoesNotStart(); - downloadAction2.post().assertDoesNotStart(); - - removeAction.finish().assertEnded(); - downloadAction1.assertStarted(); - downloadAction2.assertStarted(); - downloadAction1.finish().assertEnded(); - downloadAction2.finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { - FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); - FakeDownloadAction removeAction1 = createRemoveAction("media 1"); - FakeDownloadAction removeAction2 = createRemoveAction("media 2"); - - downloadAction.post().assertStarted(); - removeAction1.post().assertDoesNotStart(); - removeAction2.post().assertStarted(); - - downloadAction.finish().assertCancelled(); - removeAction2.finish().assertEnded(); - - removeAction1.assertStarted(); - removeAction1.finish().assertEnded(); - - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private void doTestActionRuns(FakeDownloadAction action) throws Throwable { - action.post().assertStarted().finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private void doTestActionsRunSequentially(FakeDownloadAction action1, - FakeDownloadAction action2) throws Throwable { - action1.ignoreInterrupts().post().assertStarted(); - action2.post().assertDoesNotStart(); - - action1.finish(); - action2.assertStarted(); - - action2.finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private void doTestActionsRunInParallel(FakeDownloadAction action1, - FakeDownloadAction action2) throws Throwable { - action1.post().assertStarted(); - action2.post().assertStarted(); - action1.finish().assertEnded(); - action2.finish().assertEnded(); - testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); - } - - private FakeDownloadAction createDownloadAction(String mediaId) { - return new FakeDownloadAction(downloadManager, mediaId, false); - } - - private FakeDownloadAction createRemoveAction(String mediaId) { - return new FakeDownloadAction(downloadManager, mediaId, true); - } - - private static class TestDownloadListener implements DownloadListener { - - private ConditionVariable downloadFinishedCondition; - private Throwable downloadError; - - private TestDownloadListener() { - downloadFinishedCondition = new ConditionVariable(); - } - - @Override - public void onStateChange(DownloadManager downloadManager, DownloadTask downloadTask, int state, - Throwable error) { - if (state == DownloadTask.STATE_ERROR && downloadError == null) { - downloadError = error; - } - ((FakeDownloadAction) downloadTask.getDownloadAction()).onStateChange(); - } - - @Override - public void onTasksFinished(DownloadManager downloadManager) { - downloadFinishedCondition.open(); - } - - private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { - assertTrue(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT)); - downloadFinishedCondition.close(); - if (downloadError != null) { - throw downloadError; - } - } - - } - - /** - * 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); - } - - private static class FakeDownloadAction extends DownloadAction { - - private final DownloadManager downloadManager; - private final String mediaId; - private final boolean removeAction; - private final FakeDownloader downloader; - private final ConditionVariable stateChanged; - private DownloadTask downloadTask; - - private FakeDownloadAction(DownloadManager downloadManager, String mediaId, - boolean removeAction) { - this.downloadManager = downloadManager; - this.mediaId = mediaId; - this.removeAction = removeAction; - this.downloader = new FakeDownloader(removeAction); - this.stateChanged = new ConditionVariable(); - } - - @Override - protected String getType() { - return "FakeDownloadAction"; - } - - @Override - protected void writeToStream(DataOutputStream output) throws IOException { - // do nothing. - } - - @Override - protected boolean isRemoveAction() { - return removeAction; - } - - @Override - protected boolean isSameMedia(DownloadAction other) { - return other instanceof FakeDownloadAction - && mediaId.equals(((FakeDownloadAction) other).mediaId); - } - - @Override - protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { - return downloader; - } - - private FakeDownloadAction post() { - downloadTask = downloadManager.handleAction(this); - return this; - } - - private FakeDownloadAction assertDoesNotStart() { - assertFalse(downloader.started.block(ASSERT_FALSE_TIME)); - return this; - } - - private FakeDownloadAction assertStarted() { - assertTrue(downloader.started.block(ASSERT_TRUE_TIMEOUT)); - assertState(DownloadTask.STATE_STARTED); - return this; - } - - private FakeDownloadAction assertEnded() { - return assertState(DownloadTask.STATE_ENDED); - } - - private FakeDownloadAction assertCancelled() { - return assertState(DownloadTask.STATE_CANCELED); - } - - private FakeDownloadAction assertState(@State int state) { - assertTrue(stateChanged.block(ASSERT_TRUE_TIMEOUT)); - stateChanged.close(); - assertEquals(state, downloadTask.getState()); - return this; - } - - private FakeDownloadAction finish() { - downloader.finish.open(); - return this; - } - - private FakeDownloadAction ignoreInterrupts() { - downloader.ignoreInterrupts = true; - return this; - } - - private void onStateChange() { - stateChanged.open(); - } - } - - private static class FakeDownloader implements Downloader { - private final ConditionVariable started; - private final com.google.android.exoplayer2.util.ConditionVariable finish; - private final boolean removeAction; - private boolean ignoreInterrupts; - - private FakeDownloader(boolean removeAction) { - this.removeAction = removeAction; - this.started = new ConditionVariable(); - this.finish = new com.google.android.exoplayer2.util.ConditionVariable(); - } - - @Override - public void init() throws InterruptedException, IOException { - // do nothing. - } - - @Override - public void download(@Nullable ProgressListener listener) - throws InterruptedException, IOException { - assertFalse(removeAction); - started.open(); - blockUntilFinish(); - } - - @Override - public void remove() throws InterruptedException { - assertTrue(removeAction); - started.open(); - blockUntilFinish(); - } - - private void blockUntilFinish() throws InterruptedException { - while (true){ - try { - finish.block(); - break; - } catch (InterruptedException e) { - if (!ignoreInterrupts) { - throw e; - } - } - } - } - - @Override - public long getDownloadedBytes() { - return 0; - } - - @Override - public float getDownloadPercentage() { - return 0; - } - } - -} From c9ede155521ac8da81aa74c2b8ae4c73f27c05b3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 09:36:43 -0700 Subject: [PATCH 003/148] Relax string comparison in DASH parseContentProtection ... by making it case insensitive and null-tolerant for schemeId (as was before adding playlist drm data merging). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174472123 --- .../dash/manifest/DashManifestParser.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 0c35ef0d10..72df69f7e9 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 @@ -345,30 +345,32 @@ public class DashManifestParser extends DefaultHandler */ protected Pair parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { - String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); String schemeType = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; - switch (schemeIdUri) { - case "urn:mpeg:dash:mp4protection:2011": - schemeType = xpp.getAttributeValue(null, "value"); - String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); - if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { - UUID keyId = UUID.fromString(defaultKid); - data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); - uuid = C.COMMON_PSSH_UUID; - } - break; - case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": - uuid = C.PLAYREADY_UUID; - break; - case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": - uuid = C.WIDEVINE_UUID; - break; - default: - break; + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + if (schemeIdUri != null) { + switch (schemeIdUri.toLowerCase()) { + case "urn:mpeg:dash:mp4protection:2011": + schemeType = xpp.getAttributeValue(null, "value"); + String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); + if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + UUID keyId = UUID.fromString(defaultKid); + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + uuid = C.COMMON_PSSH_UUID; + } + break; + case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": + uuid = C.PLAYREADY_UUID; + break; + case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": + uuid = C.WIDEVINE_UUID; + break; + default: + break; + } } do { From 061bcdd136d5dd7a66f6280a920b9139751ff77c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 10:01:15 -0700 Subject: [PATCH 004/148] Allow playback to continue even after SingleSampleMediaPeriod load errors This prevents users from having to check sideloaded subtitles URLs before preparing a SingleSampleMediaSource with it. Issue:#3140 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174475274 --- .../source/SingleSampleMediaPeriod.java | 40 ++++++++++++------- .../source/SingleSampleMediaSource.java | 37 +++++++++++++++-- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index b19f398d86..6101c79b7f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -52,16 +51,20 @@ import java.util.Arrays; private final int eventSourceId; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; + /* package */ final boolean treatLoadErrorsAsEndOfStream; /* package */ boolean loadingFinished; + /* package */ boolean loadingSucceeded; /* package */ byte[] sampleData; /* package */ int sampleSize; + private int errorCount; public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId) { + int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.format = format; @@ -69,6 +72,7 @@ import java.util.Arrays; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); loader = new Loader("Loader:SingleSampleMediaPeriod"); @@ -85,7 +89,7 @@ import java.util.Arrays; @Override public void maybeThrowPrepareError() throws IOException { - loader.maybeThrowError(); + // Do nothing. } @Override @@ -157,6 +161,7 @@ import java.util.Arrays; sampleSize = loadable.sampleSize; sampleData = loadable.sampleData; loadingFinished = true; + loadingSucceeded = true; } @Override @@ -169,6 +174,11 @@ import java.util.Arrays; public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { notifyLoadError(error); + errorCount++; + if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) { + loadingFinished = true; + return Loader.DONT_RETRY; + } return Loader.RETRY; } @@ -206,7 +216,9 @@ import java.util.Arrays; @Override public void maybeThrowError() throws IOException { - loader.maybeThrowError(); + if (!treatLoadErrorsAsEndOfStream) { + loader.maybeThrowError(); + } } @Override @@ -219,19 +231,19 @@ import java.util.Arrays; formatHolder.format = format; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; - } - - Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); - if (!loadingFinished) { - return C.RESULT_NOTHING_READ; - } else { - buffer.timeUs = 0; - buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); - buffer.ensureSpaceForWrite(sampleSize); - buffer.data.put(sampleData, 0, sampleSize); + } else if (loadingFinished) { + if (loadingSucceeded) { + buffer.timeUs = 0; + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + buffer.ensureSpaceForWrite(sampleSize); + buffer.data.put(sampleData, 0, sampleSize); + } else { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + } streamState = STREAM_STATE_END_OF_STREAM; return C.RESULT_BUFFER_READ; } + return C.RESULT_NOTHING_READ; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 5b190078fd..dd901958fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -57,21 +57,51 @@ public final class SingleSampleMediaSource implements MediaSource { private final Handler eventHandler; private final EventListener eventListener; private final int eventSourceId; + private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + */ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + */ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0); + this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); } + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. 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 eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated normally + * by {@link SampleStream#maybeThrowError()}. + */ public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId) { + int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.format = format; @@ -79,6 +109,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; timeline = new SinglePeriodTimeline(durationUs, true); } @@ -98,7 +129,7 @@ public final class SingleSampleMediaSource implements MediaSource { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId); + eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); } @Override From b7f13686965fd321699ade8994501103baed1f10 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 03:37:16 -0800 Subject: [PATCH 005/148] Simplify ContentDataSourceTest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174685374 --- .../upstream/ContentDataSourceTest.java | 18 +++++------------- 1 file changed, 5 insertions(+), 13 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 2b70c83ca5..6a85483dd1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,7 +23,6 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; @@ -38,9 +37,6 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - private static final 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() @@ -77,15 +73,11 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { .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)); + int testOffset = 1; + DataSpec dataSpec = new DataSpec(contentUri, testOffset, C.LENGTH_UNSET, null); + byte[] completeData = TestUtil.getByteArray(getInstrumentation(), DATA_PATH); + byte[] expectedData = Arrays.copyOfRange(completeData, testOffset, completeData.length); + TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData); } finally { dataSource.close(); } From 585e70c139de1f9afa8df5ea6a8be76be7e71dbb Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 04:02:06 -0800 Subject: [PATCH 006/148] Broaden Samsung workaround to API level 25 + J7 Issue: #3257 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174686747 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index d965b662be..ef7d691c5b 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 @@ -1141,8 +1141,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @return The mode specifying when the adaptation workaround should be enabled. */ private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { - if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name) - && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A520"))) { + if (Util.SDK_INT <= 25 && "OMX.Exynos.avc.dec.secure".equals(name) + && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A510") + || Util.MODEL.startsWith("SM-A520") || Util.MODEL.startsWith("SM-J700"))) { return ADAPTATION_WORKAROUND_MODE_ALWAYS; } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) From e6e75a536aad6d412dba4d749ea63f91a16726e6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 07:03:09 -0800 Subject: [PATCH 007/148] Don't use InputStream.available in ContentDataSource Issue: #3426 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174700804 --- .../upstream/AssetDataSourceTest.java | 4 +- .../upstream/ContentDataSourceTest.java | 110 +++++++++++++----- .../upstream/ContentDataSource.java | 17 ++- .../android/exoplayer2/testutil/TestUtil.java | 19 ++- 4 files changed, 104 insertions(+), 46 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java index 102c89ec2b..d582d25ab1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -30,14 +30,14 @@ public final class AssetDataSourceTest extends InstrumentationTestCase { AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH)); TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), true); } public void testReadAssetUri() throws Exception { AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH)); TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), true); } } 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 6a85483dd1..e19f7ad033 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 @@ -15,17 +15,22 @@ */ package com.google.android.exoplayer2.upstream; +import android.app.Instrumentation; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; @@ -37,23 +42,33 @@ 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"; - public void testReadValidUri() throws Exception { - ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); - Uri contentUri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY) - .path(DATA_PATH).build(); - DataSpec dataSpec = new DataSpec(contentUri); - TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); + public void testRead() throws Exception { + assertData(getInstrumentation(), 0, C.LENGTH_UNSET, false); + } + + public void testReadPipeMode() throws Exception { + assertData(getInstrumentation(), 0, C.LENGTH_UNSET, true); + } + + public void testReadFixedLength() throws Exception { + assertData(getInstrumentation(), 0, 100, false); + } + + public void testReadFromOffsetToEndOfInput() throws Exception { + assertData(getInstrumentation(), 1, C.LENGTH_UNSET, false); + } + + public void testReadFromOffsetToEndOfInputPipeMode() throws Exception { + assertData(getInstrumentation(), 1, C.LENGTH_UNSET, true); + } + + public void testReadFromOffsetFixedLength() throws Exception { + assertData(getInstrumentation(), 1, 100, false); } public void testReadInvalidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); - Uri contentUri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY) - .build(); + Uri contentUri = TestContentProvider.buildUri("does/not.exist", false); DataSpec dataSpec = new DataSpec(contentUri); try { dataSource.open(dataSpec); @@ -66,18 +81,16 @@ 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(); + private static void assertData(Instrumentation instrumentation, int offset, int length, + boolean pipeMode) throws IOException { + Uri contentUri = TestContentProvider.buildUri(DATA_PATH, pipeMode); + ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); try { - int testOffset = 1; - DataSpec dataSpec = new DataSpec(contentUri, testOffset, C.LENGTH_UNSET, null); - byte[] completeData = TestUtil.getByteArray(getInstrumentation(), DATA_PATH); - byte[] expectedData = Arrays.copyOfRange(completeData, testOffset, completeData.length); - TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData); + DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); + byte[] completeData = TestUtil.getByteArray(instrumentation, DATA_PATH); + byte[] expectedData = Arrays.copyOfRange(completeData, offset, + length == C.LENGTH_UNSET ? completeData.length : offset + length); + TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); } finally { dataSource.close(); } @@ -86,7 +99,21 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { /** * A {@link ContentProvider} for the test. */ - public static final class TestContentProvider extends ContentProvider { + public static final class TestContentProvider extends ContentProvider + implements ContentProvider.PipeDataWriter { + + private static final String PARAM_PIPE_MODE = "pipe-mode"; + + public static Uri buildUri(String filePath, boolean pipeMode) { + Uri.Builder builder = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .path(filePath); + if (pipeMode) { + builder.appendQueryParameter(TestContentProvider.PARAM_PIPE_MODE, "1"); + } + return builder.build(); + } @Override public boolean onCreate() { @@ -106,7 +133,14 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { return null; } try { - return getContext().getAssets().openFd(uri.getPath().replaceFirst("/", "")); + String fileName = getFileName(uri); + boolean pipeMode = uri.getQueryParameter(PARAM_PIPE_MODE) != null; + if (pipeMode) { + ParcelFileDescriptor fileDescriptor = openPipeHelper(uri, null, null, null, this); + return new AssetFileDescriptor(fileDescriptor, 0, C.LENGTH_UNSET); + } else { + return getContext().getAssets().openFd(fileName); + } } catch (IOException e) { FileNotFoundException exception = new FileNotFoundException(e.getMessage()); exception.initCause(e); @@ -125,15 +159,31 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } @Override - public int delete(@NonNull Uri uri, String selection, + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override - public int update(@NonNull Uri uri, ContentValues values, - String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); + public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, + @NonNull String mimeType, @Nullable Bundle opts, @Nullable Object args) { + try { + byte[] data = TestUtil.getByteArray(getContext(), getFileName(uri)); + FileOutputStream outputStream = new FileOutputStream(output.getFileDescriptor()); + outputStream.write(data); + outputStream.close(); + } catch (IOException e) { + throw new RuntimeException("Error writing to pipe", e); + } + } + + private static String getFileName(Uri uri) { + return uri.getPath().replaceFirst("/", ""); } } 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 c37599eccc..87642e0eba 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 @@ -24,7 +24,7 @@ import java.io.EOFException; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; +import java.nio.channels.FileChannel; /** * A {@link DataSource} for reading from a content URI. @@ -47,7 +47,7 @@ public final class ContentDataSource implements DataSource { private Uri uri; private AssetFileDescriptor assetFileDescriptor; - private InputStream inputStream; + private FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -88,14 +88,11 @@ public final class ContentDataSource implements DataSource { } else { 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) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, - // or if it's greater than Integer.MAX_VALUE. We don't know the true length in either - // case, so treat as unbounded. - bytesRemaining = C.LENGTH_UNSET; - } + // The asset must extend to the end of the file. If FileInputStream.getChannel().size() + // returns 0 then the remaining length cannot be determined. + FileChannel channel = inputStream.getChannel(); + long channelSize = channel.size(); + bytesRemaining = channelSize == 0 ? C.LENGTH_UNSET : channelSize - channel.position(); } else { bytesRemaining = assetFileDescriptorLength - skipped; } 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 b5b084fc7b..61d1ecaeea 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,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; +import android.content.Context; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; @@ -121,12 +122,20 @@ public class TestUtil { public static byte[] getByteArray(Instrumentation instrumentation, String fileName) throws IOException { - return Util.toByteArray(getInputStream(instrumentation, fileName)); + return getByteArray(instrumentation.getContext(), fileName); + } + + public static byte[] getByteArray(Context context, String fileName) throws IOException { + return Util.toByteArray(getInputStream(context, fileName)); } public static InputStream getInputStream(Instrumentation instrumentation, String fileName) throws IOException { - return instrumentation.getContext().getResources().getAssets().open(fileName); + return getInputStream(instrumentation.getContext(), fileName); + } + + public static InputStream getInputStream(Context context, String fileName) throws IOException { + return context.getResources().getAssets().open(fileName); } public static String getString(Instrumentation instrumentation, String fileName) @@ -167,13 +176,15 @@ public class TestUtil { * @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. + * @param expectKnownLength Whether to assert that {@link DataSource#open} returns the expected + * data length. If false then it's asserted that {@link C#LENGTH_UNSET} is returned. * @throws IOException If an error occurs reading fom the {@link DataSource}. */ public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec, - byte[] expectedData) throws IOException { + byte[] expectedData, boolean expectKnownLength) throws IOException { try { long length = dataSource.open(dataSpec); - Assert.assertEquals(expectedData.length, length); + Assert.assertEquals(expectKnownLength ? expectedData.length : C.LENGTH_UNSET, length); byte[] readData = TestUtil.readToEnd(dataSource); MoreAsserts.assertEquals(expectedData, readData); } finally { From 298a66fc0e2fd6c9309b8c526418651673bed5eb Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Nov 2017 09:21:01 -0800 Subject: [PATCH 008/148] Relax parsing of ctts sample deltas Issue: #3384 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174715851 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 1c4ca995f6..588282bc9b 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 @@ -247,7 +247,13 @@ import java.util.List; remainingSamplesAtTimestampDelta--; if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) { remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); - timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); + // The BMFF spec (ISO 14496-12) states that sample deltas should be unsigned integers + // in stts boxes, however some streams violate the spec and use signed integers instead. + // See https://github.com/google/ExoPlayer/issues/3384. It's safe to always decode sample + // deltas as signed integers here, because unsigned integers will still be parsed + // correctly (unless their top bit is set, which is never true in practice because sample + // deltas are always small). + timestampDeltaInTimeUnits = stts.readInt(); remainingTimestampDeltaChanges--; } From 065091cfe3c73ecb7cbe74a63c19d1216700a35a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 7 Nov 2017 02:51:43 -0800 Subject: [PATCH 009/148] Use helper method to disable Renderers. Removes duplicated code and starts cleaning up handling of media clocks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174829840 --- .../exoplayer2/ExoPlayerImplInternal.java | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b79f5c70a3..f00a5ce02d 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 @@ -718,11 +718,9 @@ import java.io.IOException; if (playingPeriodHolder != newPlayingPeriodHolder || playingPeriodHolder != readingPeriodHolder) { for (Renderer renderer : enabledRenderers) { - renderer.disable(); + disableRenderer(renderer); } enabledRenderers = new Renderer[0]; - rendererMediaClock = null; - rendererMediaClockSource = null; playingPeriodHolder = null; } @@ -801,13 +799,10 @@ import java.io.IOException; handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; standaloneMediaClock.stop(); - rendererMediaClock = null; - rendererMediaClockSource = null; rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } catch (ExoPlaybackException | RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Stop failed.", e); @@ -853,6 +848,15 @@ import java.io.IOException; } } + private void disableRenderer(Renderer renderer) throws ExoPlaybackException { + if (renderer == rendererMediaClockSource) { + rendererMediaClock = null; + rendererMediaClockSource = null; + } + ensureStopped(renderer); + renderer.disable(); + } + private void reselectTracksInternal() throws ExoPlaybackException { if (playingPeriodHolder == null) { // We don't have tracks yet, so we don't care. @@ -905,12 +909,7 @@ import java.io.IOException; if (rendererWasEnabledFlags[i]) { if (sampleStream != renderer.getStream()) { // We need to disable the renderer. - if (renderer == rendererMediaClockSource) { - rendererMediaClock = null; - rendererMediaClockSource = null; - } - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } else if (streamResetFlags[i]) { // The renderer will continue to consume from its current stream, but needs to be reset. renderer.resetPosition(rendererPositionUs); @@ -1453,12 +1452,7 @@ import java.io.IOException; // The renderer should be disabled before playing the next period, either because it's not // needed to play the next period, or because we need to re-enable it as its current stream // is final and it's not reading ahead. - if (renderer == rendererMediaClockSource) { - rendererMediaClock = null; - rendererMediaClockSource = null; - } - ensureStopped(renderer); - renderer.disable(); + disableRenderer(renderer); } } From 3472eda6dc06841c76c2562fa466782b78f1847e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Nov 2017 04:30:48 -0800 Subject: [PATCH 010/148] Be more robust against load callback failures Issue: #2795 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174836960 --- .../google/android/exoplayer2/upstream/Loader.java | 11 ++++++++++- 1 file changed, 10 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 bd70150573..9e495f42bf 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 @@ -380,7 +380,13 @@ public final class Loader implements LoaderErrorThrower { callback.onLoadCanceled(loadable, nowMs, durationMs, false); break; case MSG_END_OF_SOURCE: - callback.onLoadCompleted(loadable, nowMs, durationMs); + try { + callback.onLoadCompleted(loadable, nowMs, durationMs); + } catch (RuntimeException e) { + // This should never happen, but handle it anyway. + Log.e(TAG, "Unexpected exception handling load completed", e); + fatalError = new UnexpectedLoaderException(e); + } break; case MSG_IO_EXCEPTION: currentError = (IOException) msg.obj; @@ -392,6 +398,9 @@ public final class Loader implements LoaderErrorThrower { start(getRetryDelayMillis()); } break; + default: + // Never happens. + break; } } From 26366f6c8004377d1fff50bafd9af73c3a9b028e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 7 Nov 2017 05:22:48 -0800 Subject: [PATCH 011/148] Fix an issue with seeking that can lead to STATE_END not delivered. GitHub: #1897 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174841175 --- .../source/CompositeSequenceableLoader.java | 6 +- .../CompositeSequenceableLoaderTest.java | 274 ++++++++++++++++++ 2 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index 343d4f0bbe..a85d589762 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -63,7 +63,11 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { break; } for (SequenceableLoader loader : loaders) { - if (loader.getNextLoadPositionUs() == nextLoadPositionUs) { + long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); + boolean isLoaderBehind = + loaderNextLoadPositionUs != C.TIME_END_OF_SOURCE + && loaderNextLoadPositionUs <= positionUs; + if (loaderNextLoadPositionUs == nextLoadPositionUs || isLoaderBehind) { madeProgressThisIteration |= loader.continueLoading(positionUs); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java new file mode 100644 index 0000000000..e3ac104754 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link CompositeSequenceableLoader}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CompositeSequenceableLoaderTest { + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered + * position among all sub-loaders. + */ + @Test + public void testGetBufferedPositionUsReturnsMinimumLoaderBufferedPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns minimum buffered + * position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. + */ + @Test + public void testGetBufferedPositionUsReturnsMinimumNonEndOfSourceLoaderBufferedPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(1000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getBufferedPositionUs()} returns + * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have buffered till end-of-source. + */ + @Test + public void testGetBufferedPositionUsReturnsEndOfSourceWhenAllLoaderBufferedTillEndOfSource() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ C.TIME_END_OF_SOURCE, + /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next + * load position among all sub-loaders. + */ + @Test + public void testGetNextLoadPositionUsReturnMinimumLoaderNextLoadPositionUs() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2000); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns minimum next + * load position that is not {@link C#TIME_END_OF_SOURCE} among all sub-loaders. + */ + @Test + public void testGetNextLoadPositionUsReturnMinimumNonEndOfSourceLoaderNextLoadPositionUs() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(2000); + } + + /** + * Tests that {@link CompositeSequenceableLoader#getNextLoadPositionUs()} returns + * {@link C#TIME_END_OF_SOURCE} when all sub-loaders have next load position at end-of-source. + */ + @Test + public void testGetNextLoadPositionUsReturnsEndOfSourceWhenAllLoaderLoadingLastChunk() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + assertThat(compositeSequenceableLoader.getNextLoadPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} only allows the loader + * with minimum next load position to continue loading if next load positions are not behind + * current playback position. + */ + @Test + public void testContinueLoadingOnlyAllowFurthestBehindLoaderToLoadIfNotBehindPlaybackPosition() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + compositeSequenceableLoader.continueLoading(100); + + assertThat(loader1.numInvocations).isEqualTo(1); + assertThat(loader2.numInvocations).isEqualTo(0); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} allows all loaders + * with next load position behind current playback position to continue loading. + */ + @Test + public void testContinueLoadingReturnAllowAllLoadersBehindPlaybackPositionToLoad() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + FakeSequenceableLoader loader3 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1002, /* nextLoadPositionUs */ 2002); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2, loader3}); + compositeSequenceableLoader.continueLoading(3000); + + assertThat(loader1.numInvocations).isEqualTo(1); + assertThat(loader2.numInvocations).isEqualTo(1); + assertThat(loader3.numInvocations).isEqualTo(1); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} does not allow loader + * with next load position at end-of-source to continue loading. + */ + @Test + public void testContinueLoadingOnlyNotAllowEndOfSourceLoaderToLoad() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader( + /* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ C.TIME_END_OF_SOURCE); + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + compositeSequenceableLoader.continueLoading(3000); + + assertThat(loader1.numInvocations).isEqualTo(0); + assertThat(loader2.numInvocations).isEqualTo(0); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if the loader + * with minimum next load position can make progress if next load positions are not behind + * current playback position. + */ + @Test + public void testContinueLoadingReturnTrueIfFurthestBehindLoaderCanMakeProgress() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + loader1.setNextChunkDurationUs(1000); + + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + + assertThat(compositeSequenceableLoader.continueLoading(100)).isTrue(); + } + + /** + * Tests that {@link CompositeSequenceableLoader#continueLoading(long)} returns true if any loader + * that are behind current playback position can make progress, even if it is not the one with + * minimum next load position. + */ + @Test + public void testContinueLoadingReturnTrueIfLoaderBehindPlaybackPositionCanMakeProgress() { + FakeSequenceableLoader loader1 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1000, /* nextLoadPositionUs */ 2000); + FakeSequenceableLoader loader2 = + new FakeSequenceableLoader(/* bufferedPositionUs */ 1001, /* nextLoadPositionUs */ 2001); + // loader2 is not the furthest behind, but it can make progress if allowed. + loader2.setNextChunkDurationUs(1000); + + CompositeSequenceableLoader compositeSequenceableLoader = new CompositeSequenceableLoader( + new SequenceableLoader[] {loader1, loader2}); + + assertThat(compositeSequenceableLoader.continueLoading(3000)).isTrue(); + } + + private static class FakeSequenceableLoader implements SequenceableLoader { + + private long bufferedPositionUs; + private long nextLoadPositionUs; + private int numInvocations; + private int nextChunkDurationUs; + + private FakeSequenceableLoader(long bufferedPositionUs, long nextLoadPositionUs) { + this.bufferedPositionUs = bufferedPositionUs; + this.nextLoadPositionUs = nextLoadPositionUs; + } + + @Override + public long getBufferedPositionUs() { + return bufferedPositionUs; + } + + @Override + public long getNextLoadPositionUs() { + return nextLoadPositionUs; + } + + @Override + public boolean continueLoading(long positionUs) { + numInvocations++; + boolean loaded = nextChunkDurationUs != 0; + // The current chunk has been loaded, advance to next chunk. + bufferedPositionUs = nextLoadPositionUs; + nextLoadPositionUs += nextChunkDurationUs; + nextChunkDurationUs = 0; + return loaded; + } + + private void setNextChunkDurationUs(int nextChunkDurationUs) { + this.nextChunkDurationUs = nextChunkDurationUs; + } + + } + +} From 336c697b9de0851f93a5bff07e2f1d67e4be43b1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 7 Nov 2017 07:36:45 -0800 Subject: [PATCH 012/148] Update 2.6.0 release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174853112 --- RELEASENOTES.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 42bf526de0..9bc7005ffc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,7 @@ * Added a reason to `EventListener.onPositionDiscontinuity` ([#3252](https://github.com/google/ExoPlayer/issues/3252)). * New `setShuffleModeEnabled` method for enabling shuffled playback. +* SimpleExoPlayer: Support for multiple video, text and metadata outputs. * Support for `Renderer`s that don't consume any media ([#3212](https://github.com/google/ExoPlayer/issues/3212)). * Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` @@ -14,9 +15,12 @@ * Fix playbacks involving looping, concatenation and ads getting stuck when media contains tracks with uneven durations ([#1874](https://github.com/google/ExoPlayer/issues/1874)). +* Fix issue with `ContentDataSource` when reading from certain `ContentProvider` + implementations ([#3426](https://github.com/google/ExoPlayer/issues/3426)). * Better playback experience when the video decoder cannot keep up, by skipping to key-frames. This is particularly relevant for variable speed playbacks. -* SimpleExoPlayer: Support for multiple video, text and metadata outputs. +* Allow `SingleSampleMediaSource` to suppress load errors + ([#3140](https://github.com/google/ExoPlayer/issues/3140)). * Audio: New `AudioSink` interface allows customization of audio output path. * Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming and progressive streams. @@ -24,8 +28,14 @@ * Fixed adaptive track selection logic for live playbacks ([#3017](https://github.com/google/ExoPlayer/issues/3017)). * Added ability to select the lowest bitrate tracks. +* DASH: + * Don't crash when a malformed or unexpected manifest update occurs + ([#2795](https://github.com/google/ExoPlayer/issues/2795)). * HLS: * Support for Widevine protected FMP4 variants. + * Support CEA-608 in FMP4 variants. + * Support extractor injection + ([#2748](https://github.com/google/ExoPlayer/issues/2748)). * DRM: * Improved compatibility with ClearKey content ([#3138](https://github.com/google/ExoPlayer/issues/3138)). From 52224944ae90b07c04e44cf18b0e0f05ac8744d8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 7 Nov 2017 07:50:35 -0800 Subject: [PATCH 013/148] Fix DefaultHlsExtractorFactory javadoc Issue:#2748 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174854541 --- .../exoplayer2/source/hls/DefaultHlsExtractorFactory.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 9f0989e444..957aefcdbc 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -34,8 +34,6 @@ import java.util.List; /** * Default {@link HlsExtractorFactory} implementation. - * - *

This class can be extended to override {@link TsExtractor} instantiation.

*/ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { From 681a05d004cee9b66b58d0575f8a3d0e241995ba Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Nov 2017 07:56:16 -0800 Subject: [PATCH 014/148] Work around incorrect ClearKey encoding prior to O-MR1 Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175006223 --- .../android/exoplayer2/drm/ClearKeyUtil.java | 109 ++++++++++++++++++ .../exoplayer2/drm/DefaultDrmSession.java | 22 +++- .../exoplayer2/drm/FrameworkMediaDrm.java | 3 +- .../google/android/exoplayer2/util/Util.java | 10 ++ .../exoplayer2/drm/ClearKeyUtilTest.java | 64 ++++++++++ 5 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java new file mode 100644 index 0000000000..ee337dcc51 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ClearKeyUtil.java @@ -0,0 +1,109 @@ +/* + * 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 android.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Utility methods for ClearKey. + */ +/* package */ final class ClearKeyUtil { + + private static final String TAG = "ClearKeyUtil"; + private static final Pattern REQUEST_KIDS_PATTERN = Pattern.compile("\"kids\":\\[\"(.*?)\"]"); + + private ClearKeyUtil() {} + + /** + * Adjusts ClearKey request data obtained from the Android ClearKey CDM to be spec compliant. + * + * @param request The request data. + * @return The adjusted request data. + */ + public static byte[] adjustRequestData(byte[] request) { + if (Util.SDK_INT >= 27) { + return request; + } + // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 rather + // than Base64Url. See [Internal: b/64388098]. Any "/" characters that ended up in the request + // as a result were not escaped as "\/". We know the exact request format from the platform's + // InitDataParser.cpp, so we can use a regexp rather than parsing the JSON. + String requestString = Util.fromUtf8Bytes(request); + Matcher requestKidsMatcher = REQUEST_KIDS_PATTERN.matcher(requestString); + if (!requestKidsMatcher.find()) { + Log.e(TAG, "Failed to adjust request data: " + requestString); + return request; + } + int kidsStartIndex = requestKidsMatcher.start(1); + int kidsEndIndex = requestKidsMatcher.end(1); + StringBuilder adjustedRequestBuilder = new StringBuilder(requestString); + base64ToBase64Url(adjustedRequestBuilder, kidsStartIndex, kidsEndIndex); + return Util.getUtf8Bytes(adjustedRequestBuilder.toString()); + } + + /** + * Adjusts ClearKey response data to be suitable for providing to the Android ClearKey CDM. + * + * @param response The response data. + * @return The adjusted response data. + */ + public static byte[] adjustResponseData(byte[] response) { + if (Util.SDK_INT >= 27) { + return response; + } + // Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for + // the "k" and "kid" strings. See [Internal: b/64388098]. + try { + JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response)); + JSONArray keysArray = responseJson.getJSONArray("keys"); + for (int i = 0; i < keysArray.length(); i++) { + JSONObject key = keysArray.getJSONObject(i); + key.put("k", base64UrlToBase64(key.getString("k"))); + key.put("kid", base64UrlToBase64(key.getString("kid"))); + } + return Util.getUtf8Bytes(responseJson.toString()); + } catch (JSONException e) { + Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e); + return response; + } + } + + private static void base64ToBase64Url(StringBuilder base64, int startIndex, int endIndex) { + for (int i = startIndex; i < endIndex; i++) { + switch (base64.charAt(i)) { + case '+': + base64.setCharAt(i, '-'); + break; + case '/': + base64.setCharAt(i, '_'); + break; + default: + break; + } + } + } + + private static String base64UrlToBase64(String base64) { + return base64.replace('-', '+').replace('_', '/'); + } + +} 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 688fff48fb..c391b7035d 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 @@ -362,6 +362,20 @@ import java.util.UUID; try { KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); + if (C.CLEARKEY_UUID.equals(uuid)) { + final byte[] data = ClearKeyUtil.adjustRequestData(request.getData()); + final String defaultUrl = request.getDefaultUrl(); + request = new KeyRequest() { + @Override + public byte[] getData() { + return data; + } + @Override + public String getDefaultUrl() { + return defaultUrl; + } + }; + } postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); } catch (Exception e) { onKeysError(e); @@ -380,8 +394,12 @@ import java.util.UUID; } try { + byte[] responseData = (byte[]) response; + if (C.CLEARKEY_UUID.equals(uuid)) { + responseData = ClearKeyUtil.adjustResponseData(responseData); + } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { - mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); + mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override @@ -391,7 +409,7 @@ import java.util.UUID; }); } } else { - byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); + byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD || (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null)) && keySetId != null && keySetId.length != 0) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index c3ab3462d9..517ca9247c 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 @@ -25,6 +25,7 @@ import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -74,7 +75,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { @Override - public void onEvent(@NonNull MediaDrm md, @NonNull byte[] sessionId, int event, int extra, + public void onEvent(@NonNull MediaDrm md, @Nullable byte[] sessionId, int event, int extra, byte[] data) { listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); } 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 24132e400c..f47caa046a 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 @@ -246,6 +246,16 @@ public final class Util { return language == null ? null : new Locale(language).getLanguage(); } + /** + * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes. + * + * @param bytes The UTF-8 encoded bytes to decode. + * @return The string. + */ + public static String fromUtf8Bytes(byte[] bytes) { + return new String(bytes, Charset.forName(C.UTF8_NAME)); + } + /** * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java new file mode 100644 index 0000000000..01ab9ea9aa --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java @@ -0,0 +1,64 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import java.nio.charset.Charset; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link ClearKeyUtil}. + */ +// TODO: When API level 27 is supported, add tests that check the adjust methods are no-ops. +@RunWith(RobolectricTestRunner.class) +public final class ClearKeyUtilTest { + + @Config(sdk = 26, manifest = Config.NONE) + @Test + public void testAdjustResponseDataV26() { + byte[] data = ("{\"keys\":[{" + + "\"k\":\"abc_def-\"," + + "\"kid\":\"ab_cde-f\"}]," + + "\"type\":\"abc_def-" + + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); + // We expect "-" and "_" to be replaced with "+" and "\/" (forward slashes need to be escaped in + // JSON respectively, for "k" and "kid" only. + byte[] expected = ("{\"keys\":[{" + + "\"k\":\"abc\\/def+\"," + + "\"kid\":\"ab\\/cde+f\"}]," + + "\"type\":\"abc_def-" + + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); + assertThat(Arrays.equals(expected, ClearKeyUtil.adjustResponseData(data))).isTrue(); + } + + @Config(sdk = 26, manifest = Config.NONE) + @Test + public void testAdjustRequestDataV26() { + byte[] data = "{\"kids\":[\"abc+def/\",\"ab+cde/f\"],\"type\":\"abc+def/\"}" + .getBytes(Charset.forName(C.UTF8_NAME)); + // We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids". + byte[] expected = "{\"kids\":[\"abc-def_\",\"ab-cde_f\"],\"type\":\"abc+def/\"}" + .getBytes(Charset.forName(C.UTF8_NAME)); + assertThat(Arrays.equals(expected, ClearKeyUtil.adjustRequestData(data))).isTrue(); + } + +} From e1b3fed5e9a8743e2510c2f9d96af1cbbdece7c6 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 8 Nov 2017 08:34:20 -0800 Subject: [PATCH 015/148] Add default implementations for ExoMediaDrm.* interfaces ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175010595 --- .../exoplayer2/drm/DefaultDrmSession.java | 15 +-- .../android/exoplayer2/drm/ExoMediaDrm.java | 95 +++++++++++++++++-- .../exoplayer2/drm/FrameworkMediaDrm.java | 54 ++--------- 3 files changed, 95 insertions(+), 69 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 c391b7035d..25fdaba5b8 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 @@ -25,6 +25,7 @@ import android.os.Message; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.Arrays; @@ -363,18 +364,8 @@ import java.util.UUID; KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); if (C.CLEARKEY_UUID.equals(uuid)) { - final byte[] data = ClearKeyUtil.adjustRequestData(request.getData()); - final String defaultUrl = request.getDefaultUrl(); - request = new KeyRequest() { - @Override - public byte[] getData() { - return data; - } - @Override - public String getDefaultUrl() { - return defaultUrl; - } - }; + request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), + request.getDefaultUrl()); } postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); } catch (Exception e) { 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 e9ee1ce90b..cecc840511 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 @@ -82,14 +82,6 @@ public interface ExoMediaDrm { byte[] data); } - /** - * @see android.media.MediaDrm.KeyStatus - */ - interface ExoKeyStatus { - int getStatusCode(); - byte[] getKeyId(); - } - /** * @see android.media.MediaDrm.OnKeyStatusChangeListener */ @@ -100,11 +92,44 @@ public interface ExoMediaDrm { * * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred. * @param sessionId the DRM session ID on which the event occurred. - * @param exoKeyInfo a list of {@link ExoKeyStatus} that contains key ID and status. + * @param exoKeyInfo a list of {@link KeyStatus} that contains key ID and status. * @param hasNewUsableKey true if new key becomes usable. */ void onKeyStatusChange(ExoMediaDrm mediaDrm, byte[] sessionId, - List exoKeyInfo, boolean hasNewUsableKey); + List exoKeyInfo, boolean hasNewUsableKey); + } + + /** + * @see android.media.MediaDrm.KeyStatus + */ + interface KeyStatus { + int getStatusCode(); + byte[] getKeyId(); + } + + /** + * Default implementation of {@link KeyStatus}. + */ + final class DefaultKeyStatus implements KeyStatus { + + private final int statusCode; + private final byte[] keyId; + + DefaultKeyStatus(int statusCode, byte[] keyId) { + this.statusCode = statusCode; + this.keyId = keyId; + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public byte[] getKeyId() { + return keyId; + } + } /** @@ -115,6 +140,31 @@ public interface ExoMediaDrm { String getDefaultUrl(); } + /** + * Default implementation of {@link KeyRequest}. + */ + final class DefaultKeyRequest implements KeyRequest { + + private final byte[] data; + private final String defaultUrl; + + public DefaultKeyRequest(byte[] data, String defaultUrl) { + this.data = data; + this.defaultUrl = defaultUrl; + } + + @Override + public byte[] getData() { + return data; + } + + @Override + public String getDefaultUrl() { + return defaultUrl; + } + + } + /** * @see android.media.MediaDrm.ProvisionRequest */ @@ -123,6 +173,31 @@ public interface ExoMediaDrm { String getDefaultUrl(); } + /** + * Default implementation of {@link ProvisionRequest}. + */ + final class DefaultProvisionRequest implements ProvisionRequest { + + private final byte[] data; + private final String defaultUrl; + + public DefaultProvisionRequest(byte[] data, String defaultUrl) { + this.data = data; + this.defaultUrl = defaultUrl; + } + + @Override + public byte[] getData() { + return data; + } + + @Override + public String getDefaultUrl() { + return defaultUrl; + } + + } + /** * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener) */ 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 517ca9247c..f960cd637f 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,7 +20,6 @@ import android.media.DeniedByServerException; import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; -import android.media.MediaDrm.KeyStatus; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; @@ -93,12 +92,11 @@ public final class FrameworkMediaDrm implements ExoMediaDrm keyInfo, boolean hasNewUsableKey) { - List exoKeyInfo = new ArrayList<>(); - for (KeyStatus keyStatus : keyInfo) { - exoKeyInfo.add(new FrameworkKeyStatus(keyStatus)); + @NonNull List keyInfo, boolean hasNewUsableKey) { + List exoKeyInfo = new ArrayList<>(); + for (MediaDrm.KeyStatus keyStatus : keyInfo) { + exoKeyInfo.add(new DefaultKeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId())); } - listener.onKeyStatusChange(FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey); } @@ -120,17 +118,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, optionalParameters); - return new KeyRequest() { - @Override - public byte[] getData() { - return request.getData(); - } - - @Override - public String getDefaultUrl() { - return request.getDefaultUrl(); - } - }; + return new DefaultKeyRequest(request.getData(), request.getDefaultUrl()); } @Override @@ -141,18 +129,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Wed, 8 Nov 2017 08:45:17 -0800 Subject: [PATCH 016/148] Don't allow cancelation of non-cancelable loads Issue: #3441 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175011804 --- .../source/chunk/ChunkSampleStream.java | 28 ++++++++++++------- .../exoplayer2/source/chunk/ChunkSource.java | 3 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index b64dec59bf..8a9be92d75 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.util.Log; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -38,6 +40,8 @@ import java.util.List; public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback, Loader.ReleaseCallback { + private static final String TAG = "ChunkSampleStream"; + private final int primaryTrackType; private final int[] embeddedTrackTypes; private final boolean[] embeddedTracksSelected; @@ -318,16 +322,20 @@ public class ChunkSampleStream implements SampleStream, S boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk(); boolean canceled = false; if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { - canceled = true; - if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.removeLast(); - Assertions.checkState(removed == loadable); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); - } - if (mediaChunks.isEmpty()) { - pendingResetPositionUs = lastSeekPositionUs; + if (!cancelable) { + Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); + } else { + canceled = true; + if (isMediaChunk) { + BaseMediaChunk removed = mediaChunks.removeLast(); + Assertions.checkState(removed == loadable); + primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + } + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index 6dffc457d6..b04dc7cbdb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -85,7 +85,8 @@ public interface ChunkSource { * @param chunk The chunk whose load encountered the error. * @param cancelable Whether the load can be canceled. * @param e The error. - * @return Whether the load should be canceled. + * @return Whether the load should be canceled. Should always be false if {@code cancelable} is + * false. */ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e); From 1331f7adde9b140215ec92b90bea0abcadf94915 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 9 Nov 2017 03:14:22 -0800 Subject: [PATCH 017/148] Add custom callbacks to allows seeks after dynamic playlist modifications. These callbacks are executed on the app thread after the corresponding timeline update was triggered. This ensures that seek operations see the updated timelines and are therefore valid, even if the seek is performed into a window which didn't exist before. GitHub:#3407 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175136187 --- .../DynamicConcatenatingMediaSourceTest.java | 226 ++++++++++++++++- .../DynamicConcatenatingMediaSource.java | 228 ++++++++++++++++-- 2 files changed, 428 insertions(+), 26 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 61eebbc15a..c0c5252751 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,10 @@ */ package com.google.android.exoplayer2.source; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; @@ -40,6 +44,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; +import org.mockito.Mockito; /** * Unit tests for {@link DynamicConcatenatingMediaSource} @@ -50,6 +55,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private Timeline timeline; private boolean timelineUpdated; + private boolean customRunnableCalled; public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; @@ -371,6 +377,180 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + public void testCustomCallbackBeforePreparationAddSingle() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddMultiple() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddSingleWithIndex() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources(/* index */ 0, Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationRemove() throws InterruptedException { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.addMediaSource(createFakeMediaSource()); + + mediaSource.removeMediaSource(/* index */ 0, runnable); + verify(runnable).run(); + } + + public void testCustomCallbackBeforePreparationMove() throws InterruptedException { + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + Runnable runnable = Mockito.mock(Runnable.class); + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + verify(runnable).run(); + } + + public void testCustomCallbackAfterPreparationAddSingle() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), + runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationRemove() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + waitForTimelineUpdate(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); + } + }); + waitForCustomRunnable(); + } + + public void testCustomCallbackAfterPreparationMove() throws InterruptedException { + final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = + setUpDynamicMediaSourceOnHandlerThread(); + final Runnable runnable = createCustomRunnable(); + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( + new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + } + }); + waitForTimelineUpdate(); + + sourceHandlerPair.handler.post(new Runnable() { + @Override + public void run() { + sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, + runnable); + } + }); + waitForCustomRunnable(); + } + + private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() + throws InterruptedException { + HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); + handlerThread.start(); + Handler.Callback handlerCallback = Mockito.mock(Handler.Callback.class); + when(handlerCallback.handleMessage(any(Message.class))).thenReturn(false); + Handler handler = new Handler(handlerThread.getLooper(), handlerCallback); + final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + handler.post(new Runnable() { + @Override + public void run() { + prepareAndListenToTimelineUpdates(mediaSource); + } + }); + waitForTimelineUpdate(); + return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); + } + private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { @Override @@ -385,16 +565,41 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } private synchronized void waitForTimelineUpdate() throws InterruptedException { - long timeoutMs = System.currentTimeMillis() + TIMEOUT_MS; + long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; while (!timelineUpdated) { wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= timeoutMs) { + if (System.currentTimeMillis() >= deadlineMs) { fail("No timeline update occurred within timeout."); } } timelineUpdated = false; } + private Runnable createCustomRunnable() { + return new Runnable() { + @Override + public void run() { + synchronized (DynamicConcatenatingMediaSourceTest.this) { + assertTrue(timelineUpdated); + timelineUpdated = false; + customRunnableCalled = true; + DynamicConcatenatingMediaSourceTest.this.notify(); + } + } + }; + } + + private synchronized void waitForCustomRunnable() throws InterruptedException { + long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; + while (!customRunnableCalled) { + wait(TIMEOUT_MS); + if (System.currentTimeMillis() >= deadlineMs) { + fail("No custom runnable call occurred within timeout."); + } + } + customRunnableCalled = false; + } + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { @@ -403,6 +608,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return sources; } + private static FakeMediaSource createFakeMediaSource() { + return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + } + private static FakeTimeline createFakeTimeline(int index) { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } @@ -429,6 +638,19 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + private static class DynamicConcatenatingMediaSourceAndHandler { + + public final DynamicConcatenatingMediaSource mediaSource; + public final Handler handler; + + public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, + Handler handler) { + this.mediaSource = mediaSource; + this.handler = handler; + } + + } + private static class LazyMediaSource implements MediaSource { private Listener listener; 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 92a117ce4e..6bfa4047a5 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 @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.NonNull; -import android.util.Pair; +import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -47,6 +49,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private static final int MSG_ADD_MULTIPLE = 1; private static final int MSG_REMOVE = 2; private static final int MSG_MOVE = 3; + private static final int MSG_ON_COMPLETION = 4; // Accessed on the app thread. private final List mediaSourcesPublic; @@ -95,7 +98,22 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(MediaSource mediaSource) { - addMediaSource(mediaSourcesPublic.size(), mediaSource); + addMediaSource(mediaSourcesPublic.size(), mediaSource, null); + } + + /** + * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. + *

+ * 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. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource(MediaSource mediaSource, + @Nullable Runnable actionOnCompletion) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); } /** @@ -109,11 +127,31 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(int index, MediaSource mediaSource) { + addMediaSource(index, mediaSource, null); + } + + /** + * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. + *

+ * 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()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public synchronized void addMediaSource(int index, MediaSource mediaSource, + @Nullable Runnable actionOnCompletion) { Assertions.checkNotNull(mediaSource); Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource))); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, + new MessageData<>(index, mediaSource, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -127,7 +165,24 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * sources are added in the order in which they appear in this collection. */ public synchronized void addMediaSources(Collection mediaSources) { - addMediaSources(mediaSourcesPublic.size(), mediaSources); + addMediaSources(mediaSourcesPublic.size(), mediaSources, null); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on + * completion. + *

+ * 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. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources(Collection mediaSources, + @Nullable Runnable actionOnCompletion) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); } /** @@ -142,6 +197,24 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * sources are added in the order in which they appear in this collection. */ public synchronized void addMediaSources(int index, Collection mediaSources) { + addMediaSources(index, mediaSources, null); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. + *

+ * 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()}. + * @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. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public synchronized void addMediaSources(int index, Collection mediaSources, + @Nullable Runnable actionOnCompletion) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); @@ -149,7 +222,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - Pair.create(index, mediaSources))); + new MessageData<>(index, mediaSources, actionOnCompletion))); + } else if (actionOnCompletion != null){ + actionOnCompletion.run(); } } @@ -164,9 +239,28 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * range of 0 <= index < {@link #getSize()}. */ public synchronized void removeMediaSource(int index) { + removeMediaSource(index, null); + } + + /** + * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. + *

+ * 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()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been removed from the playlist. + */ + public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index)); + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, + new MessageData<>(index, null, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -179,13 +273,31 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * range of 0 <= index < {@link #getSize()}. */ public synchronized void moveMediaSource(int currentIndex, int newIndex) { + moveMediaSource(currentIndex, newIndex, null); + } + + /** + * Moves an existing {@link MediaSource} within the playlist and executes a custom action on + * completion. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been moved. + */ + public synchronized void moveMediaSource(int currentIndex, int newIndex, + @Nullable Runnable actionOnCompletion) { if (currentIndex == newIndex) { return; } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - Pair.create(currentIndex, newIndex))); + new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); } } @@ -215,7 +327,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); preventListenerNotification = false; - maybeNotifyListener(); + maybeNotifyListener(null); } @Override @@ -263,31 +375,42 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override @SuppressWarnings("unchecked") public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_ON_COMPLETION) { + ((EventDispatcher) message).dispatchEvent(); + return; + } preventListenerNotification = true; + EventDispatcher actionOnCompletion; switch (messageType) { case MSG_ADD: { - Pair messageData = (Pair) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, 1); - addMediaSourceInternal(messageData.first, messageData.second); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, 1); + addMediaSourceInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_ADD_MULTIPLE: { - Pair> messageData = - (Pair>) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, messageData.second.size()); - addMediaSourcesInternal(messageData.first, messageData.second); + MessageData> messageData = + (MessageData>) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, + messageData.customData.size()); + addMediaSourcesInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_REMOVE: { - shuffleOrder = shuffleOrder.cloneAndRemove((Integer) message); - removeMediaSourceInternal((Integer) message); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); + removeMediaSourceInternal(messageData.index); + actionOnCompletion = messageData.actionOnCompletion; break; } case MSG_MOVE: { - Pair messageData = (Pair) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.first); - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.second, 1); - moveMediaSourceInternal(messageData.first, messageData.second); + MessageData messageData = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.customData, 1); + moveMediaSourceInternal(messageData.index, messageData.customData); + actionOnCompletion = messageData.actionOnCompletion; break; } default: { @@ -295,14 +418,18 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } preventListenerNotification = false; - maybeNotifyListener(); + maybeNotifyListener(actionOnCompletion); } - private void maybeNotifyListener() { + private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { if (!preventListenerNotification) { listener.onSourceInfoRefreshed(this, new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); + if (actionOnCompletion != null) { + player.sendMessages( + new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + } } } @@ -359,7 +486,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } mediaSourceHolder.isPrepared = true; - maybeNotifyListener(); + maybeNotifyListener(null); } private void removeMediaSourceInternal(int index) { @@ -407,6 +534,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl return index; } + /** + * Data class to hold playlist media sources together with meta data needed to process them. + */ private static final class MediaSourceHolder implements Comparable { public final MediaSource mediaSource; @@ -432,6 +562,47 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } + /** + * Can be used to dispatch a runnable on the thread the object was created on. + */ + private static final class EventDispatcher { + + public final Handler eventHandler; + public final Runnable runnable; + + public EventDispatcher(Runnable runnable) { + this.runnable = runnable; + this.eventHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() + : Looper.getMainLooper()); + } + + public void dispatchEvent() { + eventHandler.post(runnable); + } + + } + + /** + * Message used to post actions from app thread to playback thread. + */ + private static final class MessageData { + + public final int index; + public final CustomType customData; + public final @Nullable EventDispatcher actionOnCompletion; + + public MessageData(int index, CustomType customData, @Nullable Runnable actionOnCompletion) { + this.index = index; + this.actionOnCompletion = actionOnCompletion != null + ? new EventDispatcher(actionOnCompletion) : null; + this.customData = customData; + } + + } + + /** + * Timeline exposing concatenated timelines of playlist media sources. + */ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { private final int windowCount; @@ -514,6 +685,10 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the + * DeferredTimeline is used to keep the originally assigned first period ID. + */ private static final class DeferredTimeline extends Timeline { private static final Object DUMMY_ID = new Object(); @@ -582,6 +757,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } + /** + * Media period used for periods created from unprepared media sources exposed through + * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes + * available. + */ private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { public final MediaSource mediaSource; From fbfbe7d6c9313b7a9b8fc26e8483989351336320 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 9 Nov 2017 07:14:21 -0800 Subject: [PATCH 018/148] Re-enable index file store at the end of the SimpleCache.initialize() In the case converting cache files from an earlier version of SimpleCache, there is no previous version of the index file. If the app doesn't call any SimpleCache methods which would make the index file stored before it exists whole data gets lost. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175153650 --- .../android/exoplayer2/upstream/cache/SimpleCache.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 9739f21923..599474d6c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.File; @@ -31,6 +32,8 @@ import java.util.TreeSet; */ public final class SimpleCache implements Cache { + private static final String TAG = "SimpleCache"; + private final File cacheDir; private final CacheEvictor evictor; private final HashMap lockedSpans; @@ -288,7 +291,11 @@ public final class SimpleCache implements Cache { } index.removeEmpty(); - // Don't call index.store() here so initialization doesn't fail because of write issues. + try { + index.store(); + } catch (CacheException e) { + Log.e(TAG, "Storing index file failed", e); + } } /** From 6e15d5cab7f3de87ca0ef73b5a9974badcdb1711 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 09:31:52 -0800 Subject: [PATCH 019/148] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175542973 --- extensions/tv/README.md | 41 -------------------------------------- extensions/tv/build.gradle | 39 ------------------------------------ 2 files changed, 80 deletions(-) delete mode 100644 extensions/tv/README.md delete mode 100644 extensions/tv/build.gradle diff --git a/extensions/tv/README.md b/extensions/tv/README.md deleted file mode 100644 index 0deb33794f..0000000000 --- a/extensions/tv/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# ExoPlayer TV tuner extension # - -Provides components for broadcast TV playback with ExoPlayer. - -## Links ## - -* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.tv.*` - belong to this extension. - -[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html - -## Build Instructions ## - -* Checkout ExoPlayer: - -``` -git clone https://github.com/google/ExoPlayer.git -``` - -* Set the following environment variables: - -``` -cd "" -EXOPLAYER_ROOT="$(pwd)" -TV_MODULE_PATH="${EXOPLAYER_ROOT}/extensions/tv/src/main" -``` - -* Download the [Android NDK][] and set its location in an environment variable: - -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - -``` -NDK_PATH="" -``` - -* Build the JNI native libraries from the command line: - -``` -cd "${TV_MODULE_PATH}"/jni && \ -${NDK_PATH}/ndk-build APP_ABI=all -j -``` diff --git a/extensions/tv/build.gradle b/extensions/tv/build.gradle deleted file mode 100644 index ee54926650..0000000000 --- a/extensions/tv/build.gradle +++ /dev/null @@ -1,39 +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. -apply from: '../../constants.gradle' -apply plugin: 'com.android.library' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion 21 - targetSdkVersion project.ext.targetSdkVersion - } - - sourceSets.main { - jniLibs.srcDir 'src/main/libs' - jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. - } -} - -dependencies { - compile project(modulePrefix + 'library-core') -} - -ext { - javadocTitle = 'TV tuner extension' -} -apply from: '../../javadoc_library.gradle' From 812ecc3c2dc0d451571c91bc589cca2b4b5b25c6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 09:35:49 -0800 Subject: [PATCH 020/148] Drop the 'r' from release version. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175543465 --- RELEASENOTES.md | 3 ++- constants.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9bc7005ffc..6e6ed28077 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,7 +1,8 @@ # Release notes # -### r2.6.0 ### +### 2.6.0 ### +* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". * New `Player.DefaultEventListener` abstract class can be extended to avoid having to implement all methods defined by `Player.EventListener`. * Added a reason to `EventListener.onPositionDiscontinuity` diff --git a/constants.gradle b/constants.gradle index 644d47b8aa..2a7754d65c 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.6.0' + releaseVersion = '2.6.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix From 4ac8680b947f6a2d826d916bc49d4a808ce0ea0f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 10:01:30 -0800 Subject: [PATCH 021/148] Update 2.6.0 release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175546817 --- RELEASENOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6e6ed28077..d6b20be4e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,9 @@ to key-frames. This is particularly relevant for variable speed playbacks. * Allow `SingleSampleMediaSource` to suppress load errors ([#3140](https://github.com/google/ExoPlayer/issues/3140)). +* `DynamicConcatenatingMediaSource`: Allow specifying a callback to be invoked + after a dynamic playlist modification has been applied + ([#3407](https://github.com/google/ExoPlayer/issues/3407)). * Audio: New `AudioSink` interface allows customization of audio output path. * Offline: Added `Downloader` implementations for DASH, HLS, SmoothStreaming and progressive streams. From 2c8c14d647025817db2a70c3b08763f348907dfd Mon Sep 17 00:00:00 2001 From: yqritc Date: Wed, 8 Nov 2017 11:01:47 +0900 Subject: [PATCH 022/148] add bravia workaround to skip using setOutputSurface in MediaCodec --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 3 ++- 1 file changed, 2 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 25e507d984..ac196da977 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 @@ -1075,7 +1075,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/3355. return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name)) - || ("tcl_eu".equals(Util.DEVICE) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); + || (("tcl_eu".equals(Util.DEVICE) || Util.MODEL.startsWith("BRAVIA")) + && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); } /** From 2c495502e020041cb999c41e92e375d2a2be808d Mon Sep 17 00:00:00 2001 From: yqritc Date: Wed, 8 Nov 2017 13:41:04 +0900 Subject: [PATCH 023/148] add issue link --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index ac196da977..2d74dfba9e 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 @@ -1071,8 +1071,9 @@ 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 and - // https://github.com/google/ExoPlayer/issues/3355. + // Work around https://github.com/google/ExoPlayer/issues/3236, + // https://github.com/google/ExoPlayer/issues/3355 and + // https://github.com/google/ExoPlayer/issues/3439. return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name)) || (("tcl_eu".equals(Util.DEVICE) || Util.MODEL.startsWith("BRAVIA")) From a573099f93a4a77c7c08f4cb2ee2345446e59b47 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 9 Nov 2017 10:56:47 +0900 Subject: [PATCH 024/148] update bravia workaround more precisely --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 3 ++- 1 file changed, 2 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 2d74dfba9e..41e3c970c4 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 @@ -1076,7 +1076,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/3439. return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name)) - || (("tcl_eu".equals(Util.DEVICE) || Util.MODEL.startsWith("BRAVIA")) + || (("tcl_eu".equals(Util.DEVICE) || "SVP-DTV15".equals(Util.DEVICE) + || "BRAVIA_ATV2".equals(Util.DEVICE)) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); } From 427fe668552c2659d94c2e99a766dd7d7349090b Mon Sep 17 00:00:00 2001 From: nvalletta Date: Sun, 12 Nov 2017 14:23:22 -0700 Subject: [PATCH 025/148] Add an easy way to set the shutter view background color --- .../exoplayer2/ui/SimpleExoPlayerView.java | 22 +++++++++++++++++++ library/ui/src/main/res/values/attrs.xml | 1 + 2 files changed, 23 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 fc41031756..86fe7a3f7f 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 @@ -83,6 +83,12 @@ import java.util.List; *

  • Default: {@code true}
  • * * + *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} view. + *
      + *
    • Corresponding method: {@link #setShutterBackgroundColor(int)}
    • + *
    • Default: {@code 0}
    • + *
    + *
  • *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. *
      *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
    • @@ -249,6 +255,7 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } + int shutterColor = 0; int playerLayoutId = R.layout.exo_simple_player_view; boolean useArtwork = true; int defaultArtworkId = 0; @@ -262,6 +269,7 @@ public final class SimpleExoPlayerView extends FrameLayout { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { + shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); @@ -293,6 +301,9 @@ public final class SimpleExoPlayerView extends FrameLayout { // Shutter view. shutterView = findViewById(R.id.exo_shutter); + if (shutterView != null) { + shutterView.setBackgroundColor(shutterColor); + } // Create a surface view and insert it into the content frame, if there is one. if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { @@ -513,6 +524,17 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + /** + * Sets the background color of the {@code exo_shutter} view. + * + * @param color A resolved color (not a resource ID) for the background of the shutter view. + */ + public void setShutterBackgroundColor(int color) { + if (shutterView != null) { + shutterView.setBackgroundColor(color); + } + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { if (player != null && player.isPlayingAd()) { diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index d02d54ef23..eb9edaccdc 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -43,6 +43,7 @@ + From 444358521c9532f5e98f492f3cd5200d4bffd920 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 13 Nov 2017 17:28:25 +0000 Subject: [PATCH 026/148] Only set shutter color if attr is declared --- .../exoplayer2/ui/SimpleExoPlayerView.java | 22 +++++++++++-------- library/ui/src/main/res/values/attrs.xml | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) 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 86fe7a3f7f..b09e80c591 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 @@ -83,12 +83,6 @@ import java.util.List; *
    • Default: {@code true}
    • *
    *
  • - *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} view. - *
      - *
    • Corresponding method: {@link #setShutterBackgroundColor(int)}
    • - *
    • Default: {@code 0}
    • - *
    - *
  • *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. *
      *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
    • @@ -119,6 +113,13 @@ import java.util.List; *
    • Default: {@code surface_view}
    • *
    *
  • + *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} + * view. + *
      + *
    • Corresponding method: {@link #setShutterBackgroundColor(int)}
    • + *
    • Default: {@code unset}
    • + *
    + *
  • *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
      @@ -255,6 +256,7 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } + boolean shutterColorSet = false; int shutterColor = 0; int playerLayoutId = R.layout.exo_simple_player_view; boolean useArtwork = true; @@ -269,7 +271,9 @@ public final class SimpleExoPlayerView extends FrameLayout { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { - shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); + shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); + shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, + shutterColor); playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); @@ -301,7 +305,7 @@ public final class SimpleExoPlayerView extends FrameLayout { // Shutter view. shutterView = findViewById(R.id.exo_shutter); - if (shutterView != null) { + if (shutterView != null && shutterColorSet) { shutterView.setBackgroundColor(shutterColor); } @@ -527,7 +531,7 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Sets the background color of the {@code exo_shutter} view. * - * @param color A resolved color (not a resource ID) for the background of the shutter view. + * @param color The background color. */ public void setShutterBackgroundColor(int color) { if (shutterView != null) { diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index eb9edaccdc..525f95768c 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -43,7 +43,7 @@ - + From 47a9609d3f449a94d761492600bd575de217ebb3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 17 Nov 2017 19:26:55 +0000 Subject: [PATCH 027/148] Remove cast extension from 2.6.0 release --- core_settings.gradle | 2 - demos/cast/README.md | 4 - demos/cast/build.gradle | 51 -- demos/cast/src/main/AndroidManifest.xml | 42 - .../android/exoplayer2/castdemo/DemoUtil.java | 92 -- .../exoplayer2/castdemo/MainActivity.java | 122 --- .../exoplayer2/castdemo/PlayerManager.java | 208 ----- .../src/main/res/layout/main_activity.xml | 42 - demos/cast/src/main/res/menu/menu.xml | 25 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 4315 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2734 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 6159 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 10578 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 11743 -> 0 bytes demos/cast/src/main/res/values/strings.xml | 25 - extensions/cast/README.md | 33 - extensions/cast/build.gradle | 45 - extensions/cast/src/main/AndroidManifest.xml | 16 - .../exoplayer2/ext/cast/CastPlayer.java | 853 ------------------ .../exoplayer2/ext/cast/CastTimeline.java | 114 --- .../exoplayer2/ext/cast/CastUtils.java | 94 -- .../ext/cast/DefaultCastOptionsProvider.java | 42 - settings.gradle | 2 - 23 files changed, 1812 deletions(-) delete mode 100644 demos/cast/README.md delete mode 100644 demos/cast/build.gradle delete mode 100644 demos/cast/src/main/AndroidManifest.xml delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java delete mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java delete mode 100644 demos/cast/src/main/res/layout/main_activity.xml delete mode 100644 demos/cast/src/main/res/menu/menu.xml delete mode 100644 demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 demos/cast/src/main/res/values/strings.xml delete mode 100644 extensions/cast/README.md delete mode 100644 extensions/cast/build.gradle delete mode 100644 extensions/cast/src/main/AndroidManifest.xml delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java delete mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java diff --git a/core_settings.gradle b/core_settings.gradle index 20a7c87bde..7a8320b1a1 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -28,7 +28,6 @@ include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-ima' -include modulePrefix + 'extension-cast' include modulePrefix + 'extension-mediasession' include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-opus' @@ -47,7 +46,6 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') -project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') diff --git a/demos/cast/README.md b/demos/cast/README.md deleted file mode 100644 index 2c68a5277a..0000000000 --- a/demos/cast/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Cast demo application # - -This folder contains a demo application that showcases ExoPlayer integration -with Google Cast. diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle deleted file mode 100644 index a9fa27ad58..0000000000 --- a/demos/cast/build.gradle +++ /dev/null @@ -1,51 +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. -apply from: '../../constants.gradle' -apply plugin: 'com.android.application' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion 16 - targetSdkVersion project.ext.targetSdkVersion - } - - buildTypes { - release { - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt') - } - debug { - jniDebuggable = true - } - } - - lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' - } - -} - -dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'extension-cast') -} diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml deleted file mode 100644 index 11f8e39b53..0000000000 --- a/demos/cast/src/main/AndroidManifest.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java deleted file mode 100644 index d36f8c319e..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.castdemo; - -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.gms.cast.MediaInfo; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Utility methods and constants for the Cast demo application. - */ -/* package */ final class DemoUtil { - - public static final String MIME_TYPE_DASH = "application/dash+xml"; - public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl"; - public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml"; - public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; - - /** - * The list of samples available in the cast demo app. - */ - public static final List SAMPLES; - - /** - * Represents a media sample. - */ - public static final class Sample { - - /** - * The uri from which the media sample is obtained. - */ - public final String uri; - /** - * A descriptive name for the sample. - */ - public final String name; - /** - * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. - */ - public final String mimeType; - - /** - * @param uri See {@link #uri}. - * @param name See {@link #name}. - * @param mimeType See {@link #mimeType}. - */ - public Sample(String uri, String name, String mimeType) { - this.uri = uri; - this.name = name; - this.mimeType = mimeType; - } - - @Override - public String toString() { - return name; - } - - } - - static { - // App samples. - ArrayList samples = new ArrayList<>(); - samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "DASH (clear,MP4,H264)", MIME_TYPE_DASH)); - samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" - + "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS)); - samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", - MIME_TYPE_VIDEO_MP4)); - - - SAMPLES = Collections.unmodifiableList(samples); - - } - - private DemoUtil() {} - -} 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 deleted file mode 100644 index 094e9f9e6e..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.castdemo; - -import android.graphics.Color; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AppCompatActivity; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; -import com.google.android.gms.cast.framework.CastButtonFactory; - -/** - * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. - */ -public class MainActivity extends AppCompatActivity { - - private SimpleExoPlayerView simpleExoPlayerView; - private PlaybackControlView castControlView; - private PlayerManager playerManager; - - // Activity lifecycle methods. - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.main_activity); - - simpleExoPlayerView = findViewById(R.id.player_view); - simpleExoPlayerView.requestFocus(); - - castControlView = findViewById(R.id.cast_control_view); - - ListView sampleList = findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleListAdapter()); - sampleList.setOnItemClickListener(new SampleClickListener()); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.menu, menu); - CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, - R.id.media_route_menu_item); - return true; - } - - @Override - public void onResume() { - super.onResume(); - playerManager = new PlayerManager(simpleExoPlayerView, castControlView, this); - } - - @Override - public void onPause() { - super.onPause(); - playerManager.release(); - playerManager = null; - } - - // Activity input. - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - // If the event was not handled then see if the player view can handle it. - return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); - } - - // User controls. - - private final class SampleListAdapter extends ArrayAdapter { - - public SampleListAdapter() { - super(getApplicationContext(), android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); - } - - @Override - @NonNull - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - view.setBackgroundColor(Color.WHITE); - return view; - } - - } - - private class SampleClickListener implements AdapterView.OnItemClickListener { - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (parent.getSelectedItemPosition() != position) { - DemoUtil.Sample currentSample = DemoUtil.SAMPLES.get(position); - playerManager.setCurrentSample(currentSample, 0, true); - } - } - - } - -} 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 deleted file mode 100644 index 77dc018a73..0000000000 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.castdemo; - -import android.content.Context; -import android.net.Uri; -import android.view.KeyEvent; -import android.view.View; -import com.google.android.exoplayer2.DefaultRenderersFactory; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -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; - -/** - * Manages players for the ExoPlayer/Cast integration app. - */ -/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener { - - private static final int PLAYBACK_REMOTE = 1; - private static final int PLAYBACK_LOCAL = 2; - - private static final String USER_AGENT = "ExoCastDemoPlayer"; - private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); - private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = - new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); - - private final SimpleExoPlayerView exoPlayerView; - private final PlaybackControlView castControlView; - private final SimpleExoPlayer exoPlayer; - private final CastPlayer castPlayer; - - private int playbackLocation; - private DemoUtil.Sample currentSample; - - /** - * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. - * @param castControlView The {@link PlaybackControlView} to control remote playback. - * @param context A {@link Context}. - */ - public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, - Context context) { - this.exoPlayerView = exoPlayerView; - this.castControlView = castControlView; - - DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); - exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); - exoPlayerView.setPlayer(exoPlayer); - - castPlayer = new CastPlayer(CastContext.getSharedInstance(context)); - castPlayer.setSessionAvailabilityListener(this); - castControlView.setPlayer(castPlayer); - - setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL); - } - - /** - * Starts playback of the given sample at the given position. - * - * @param currentSample The {@link DemoUtil} to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. - */ - public void setCurrentSample(DemoUtil.Sample currentSample, long positionMs, - boolean playWhenReady) { - this.currentSample = currentSample; - if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); - castPlayer.setPlayWhenReady(playWhenReady); - } else /* playbackLocation == PLAYBACK_LOCAL */ { - exoPlayer.prepare(buildMediaSource(currentSample), true, true); - exoPlayer.setPlayWhenReady(playWhenReady); - exoPlayer.seekTo(positionMs); - } - } - - /** - * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current - * playback location. - * - * @param event The {@link KeyEvent}. - * @return Whether the event was handled by the target view. - */ - public boolean dispatchKeyEvent(KeyEvent event) { - if (playbackLocation == PLAYBACK_REMOTE) { - return castControlView.dispatchKeyEvent(event); - } else /* playbackLocation == PLAYBACK_REMOTE */ { - return exoPlayerView.dispatchKeyEvent(event); - } - } - - /** - * Releases the manager and the players that it holds. - */ - public void release() { - castPlayer.setSessionAvailabilityListener(null); - castPlayer.release(); - exoPlayerView.setPlayer(null); - exoPlayer.release(); - } - - // CastPlayer.SessionAvailabilityListener implementation. - - @Override - public void onCastSessionAvailable() { - setPlaybackLocation(PLAYBACK_REMOTE); - } - - @Override - public void onCastSessionUnavailable() { - setPlaybackLocation(PLAYBACK_LOCAL); - } - - // Internal methods. - - private static MediaQueueItem buildMediaQueueItem(DemoUtil.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(DemoUtil.Sample sample) { - Uri uri = Uri.parse(sample.uri); - switch (sample.mimeType) { - case DemoUtil.MIME_TYPE_SS: - return new SsMediaSource(uri, DATA_SOURCE_FACTORY, - new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); - case DemoUtil.MIME_TYPE_DASH: - return new DashMediaSource(uri, DATA_SOURCE_FACTORY, - new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); - case DemoUtil.MIME_TYPE_HLS: - return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null); - case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), - null, null); - default: { - throw new IllegalStateException("Unsupported type: " + sample.mimeType); - } - } - } - - private void setPlaybackLocation(int playbackLocation) { - if (this.playbackLocation == playbackLocation) { - return; - } - - // View management. - if (playbackLocation == PLAYBACK_LOCAL) { - exoPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else { - exoPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - long playbackPositionMs; - boolean playWhenReady; - if (this.playbackLocation == PLAYBACK_LOCAL) { - playbackPositionMs = exoPlayer.getCurrentPosition(); - playWhenReady = exoPlayer.getPlayWhenReady(); - exoPlayer.stop(); - } else /* this.playbackLocation == PLAYBACK_REMOTE */ { - playbackPositionMs = castPlayer.getCurrentPosition(); - playWhenReady = castPlayer.getPlayWhenReady(); - castPlayer.stop(); - } - - this.playbackLocation = playbackLocation; - if (currentSample != null) { - setCurrentSample(currentSample, playbackPositionMs, playWhenReady); - } - } - -} diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml deleted file mode 100644 index 5d94931b64..0000000000 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/demos/cast/src/main/res/menu/menu.xml b/demos/cast/src/main/res/menu/menu.xml deleted file mode 100644 index 075ad34ec4..0000000000 --- a/demos/cast/src/main/res/menu/menu.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 52e8dc93d9895cf4e991eb391eb3f608d89b4743..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4315 zcmV<15G3!3P)~6bRYXAG+QmeMh z<9=$p6+cwf=e|ixvhG*soC~t_&w2dk_|E|#l$=g3AmQX@a)-d%4)=a{y7yeEy*xG6 z+KGgCKc%)Hesl-`{_5@Yi)1jFM%FerHyk1eSy>IOe`%e4JA@*uy0*RSWtpYCQ}Hd^afC+ zE2p`cz)Q0sf_CJ%u|3<3ty${l{sK3OYh^fH`Vqy*ud>-}eRl5L8A_yMtZ82wdd zvpKP6&z_x#6Gq+(q!%Yt@M2kG79Y9snvH zk6MrNtC@DWs;C2uCKCnfMH1^#L0w(lQ$+H`y9^0wmmr4c4HUC4ptNeLfP3?Ma4&xe z?iCRNwqNFFB;@Ubpp0vMZnsG&Ct%${~{l+%4e^KjtK|cPww>~v{@h@okK^F$kwIks>8y+96ikh(0f%+y@ zl8CCEEaE#zwOK!k+-|q4s;X+>lP`7eMA|2aH@S;)^7ZAk6cjB)=yx8ZQ9^>#?E#>E{;}1GXUALd(j+UkX1W1* zP}itNktp0|n-a*-kJ{SW|J|@*eV4QQB80*_5y=;=(Gn3oFHw$}H!@0)zNpm+kPl9V zQlH8DDZbMJF=DX@s|8y8TNLAw3mtg$ODjH2aR_N}kwZml6qVKM3epO;B`+^GLXz@A z7)1if7q!uXii(P^ym$!(!B9afBOtHxrIoZ3qSp6;oHwd7C#v0=l8Fk1bMf?$*&A)tO*GRT1hM0d^P{lk*+eY9 zOq1h3i8zllh`XblaKUT=#1UZVtwj^+%D!;&k7l-5b@mW=#QVj-_q&lhQz|7k4N zKboXQT6i)FpN~aDOe9Qiy#d?&d628Cg(OW>&~UXcXJ%%;OC(=>{~^-rzhqgSM?o-3 zkZV~4cE=|$Qv3@N-|dV}K+}RKRD2VI?GcGcyE+-#HA}CepcmrM5EqU5m?+f8M#DU4 z037SqL1}7&2MAh_DAwHeSTuU{=+lU#;ccjJW*N?tGl^k>oEed#!+r&6q*KWHN0PAY zCQXc>6+y9Hq9akC7>~v=U&3|hAUtkY(4y+=>vPtuS#u4MG_;05?Dss+6B~^bBsln5 zysjYPQ={|+`HE`j-4ji}{tS0vVNlyDRvx=|?|zv`8s3HqH+{tY$=i0@RgZM%qq_VFac1Zne`CeCqA_KU;P z8$^xqFK6}jNyvXTR&H`O=LqGrg^Cd(QNLpjxzI|6Q z#IlVNB*ZwI&L;O9!ObpaI$%&aWWO zrpso`m~j`81d`TRwrtt8e1nl4tf1}rCM3-|iFlGU{TM#{Yz2OJ{xhUp)$4CYQ#y-u z=98zWNb}P?co1BN5BZg&397EH&YLu8(hWosNLqu1`f^Sb_ZuZBVde=4;u%dnemjH_ zedl1_O>g*>w3w{CE(u~JtBdg&;)4;xVczuio4%`m-9D6+l^q^7Y}l1V5=dHO`0(N9 zaVfHv77Z075Yg1@=lz)a&>*Ip|0vSdUEA+UgqR$jYIFn%lBONS=srIoCA??Aqng-1N+qH# zq!sBcHMA9eLL4lge+uW`z3@1lZC+|{x-%;zB%~{m1d`U^(Yd&|csNgNHZ;Z9?)ULh zEV%A}#HbeJVpXTk^y>@zs+CYImUeA@u*6{V=FOi77g8%n=-$11qiC_wNI{7+k7HuO z6y1@Gh;xL*z_&ZJI`jH#u+IJiaz#bEcNs-#F`LcSIdkU33m2^k3Yj`}>aDbxWu&0w zsYmeRbD!!8T6j$#{Px&T%(>+)ZT_UYVb;(+8BJeLfh#i;9!1d?qa%m~I&Z{?5qAg| ztqOYZ!3WRdT4a%tg2Yh&O~hDDkl)Dp=9Mw%^~(XgH+CJqjvR}H*Y;5h`sFPIi8>P> z3(KgHaPHUv#o^FNr@-sa;lqd1ZoKiv3;YS{L{8&LeHBG<8(D)!d^87(yLI;~NEGiO zgBEd0kO^_Vh`NEpQ}OHL!+Bi~EDcZcZrVq|^uhbEXQV?hne;cs+7LY)!R55Hv_Zmw z>Z0`{?1vwIxSuRZ1?_YAAWrH6E%u_YYf||c#6uy*m@L z@B08nNw32;eJb1~CGD#-I)b^wLYuFDfdUW%NAgwXe4`b;e=A z^{-*^mE9pCN-RUB#7xAi-|j@bc8w2lq}QA*%uG81%kgY@WViN*4Ti!M1h~|ibMCq4 zUf^4x{ia!JYHB=-waF+!0xfmkAH4>{`_9F%e)I4y>rL{sBY%<;H{%5FaRQ&NEX1xt zGwiYl%?Lt_{vMF|`|rO`)=Mw#2z&V9hr5-Olq@q+kgq7#8kLrjHbbS~l3Xl1UJH}W z4KxQULBBZf-@kw5?YG|^uBDgW5kk5yTC}Jq3zyj#LHdZOd1F4)X?G<`O%C<-anN$~ zqo<~(#=_|7!#HKK?%dBsAN8Zh|A_YAoL%2^ z^ytx4x7)2Rh`)G$u?0PtGOef6alAS=3%~6sM^U|<*BGBpfxbayYGvGU#~n9|KB|3f z-w{gA>f5*PQw+yLoMG$!olR|%3qv#V5j*`jeCteiPW;BLEykhJM!01qh;#~m)Z&}u zBd@*o+B2eWdh?z@Q|*EY6DB0{FVYKiC6U9FFl=c)MDZ1f_J-^RG&yA;*cosTh-c@k z<)TreMs;VOE(|=i^i!=1x%8J`etEaDva&)q$4MO{=|D*%4pJ8DjQ(GJbpYJ&s^s?a z`|QhQq)Qu9OMh+@8XDT|_uqf-SHUBYbmI?!ex2!n$PseB{r1}d+#KvC`l2&xwYwgj z-nnzqAxLzyJ=1Bj@h-c(Jb0McJAB2U4T_yUUQ+I-vbOyQdlTSYB$%*7)jtI>> z{^7)kXX1LYnIrHKuF`w)nY)Q+H%N|tB$3N|_wM~Xhs$)%DIEW3g7}Wzv1!w$jHINb z7sRs~A36Fp>HnIw0?ve@FlwuE|<%X_#yW`7z~EUzs!+mTGFu9jthPn$-0ZhVu{Sc9EK0O-R=it zGPa^9K5~&$xQ)7vnl+5F$#8dDtyX_57K`G)(;XJ|60!t!T>Iqlk6Rm=fEjzcK3@WdKaF#o7U#JdDfC%*?x9-!L%k-lrv_#VD{ ziJrh+sGLf?Ob_2ms4S?LsC+NJCvock(?B@!Fg>gw1mU3?Dd1-i;r~+ubmLq2>BjDM4V6tA8agFxPWeyg zDpJb79S+B-gxDX_bOf6S$^hMn#ZUUj0(DuLppCGBU2Ph+?NYJ6M#aWj6+2rsI9&mw zAyrkK+}YWADj^u3@S}q=Af-HpXFjla30c1k+S+%BcOiGC!L67<9u8?j)x21{Bx;&9 zHiD8m4Gtx+38&NfcU@iGv4mhq+hK=HA*0b4M#A3?m^LiZS_gIMFsK>N5YIx*iiWoS zBgp3UaBGTgB%t`oD;4~etzh#G6^iOct3_f1MWVD`gG&t*HK|yw)^{gPnjB69R|9k* zuQttrM6fv7UI;ZO9%^Q!j@L*|BDC$lK^m)k=X(4MjLKJ#l;S|dFLtEnDI5tjn+3os+AI=IGV*JN6Y+mKu%ksoJ00VC=(I_AJOR>ARkhaE z)>nj>9!>}yZ|8#w<}5mSGl7r{kZ4y%By6*z7#{O6UwylR2!1=}F8meR#+ zmnhz&;hCT9NZaZ{7ZvdaGEmd#lvI| zTnCI(acR5uGz=$%jy~We>S!tDVm>hx1Im&}Y)VeV{PU6zV9S66xY8q`Wk

      KMFM; z#bU+%31}Gn0-W>aKz6jl18yEYW%QZv7Fr?G=FXja3?Uey4{SEu^>jjGCN5Kji!gwKd^&;Z!Sj)7WN$9o6U&FhFnQ&ZDO)-3e4Zr!?L z$ysj51~R=l-t=1x(yul6M}b&$Q8IF_qT;^VfP!lbXpHbhV1Htx!A49ky@;+w3n7ih zZllrB(UG5$l5!j&1b3B{m1k<2Rv4-Qt^qF8`k!KvdA+}ZjLQrtdoUg?QL$)^jzx21 zEIL?lAoR3~kA>x<51_1B4e4|SjE2c%Dp|O2;n{=`+||_7TtWtR25Z3GUI>j0bdHPW z)h1vKETkGJzAr&fm(bv}G94X69vt5Zu+))?Pyf8iO(bpsS$- zWpDRK_D#He`ip1MsbK4=V0&UfPb=eqeo(6_{nCk6%%;4&yz2=exT~nBxHeb=fCm+O zI`QQj(=qXu*Rb$>1LJ2vUvYY6U|@f!)z#1q=n-H8Sy@@v6GCveY15|5sEC6~A$k>w<>X!`1w5f0_ieCzu&F0-RXU@EW5P~~0 zagL%WMWGnzKeZeQlgp7fsSK}vk%{ToKYwr}D2)s#q20HMikAzyF@lOX_7Fv!5C_W_ zpF!EQ32v9muZYP2uR^7N{q@&*gb>_)^UXIW(hip&ssR?yCf@jX31(e2jB0@m_!o25 z6$a=_X~VODmr=(2`m5;5%7nC9x?M&a8ylAm88YM~LJ01t9d0xlM}-`LN#z*$pUoI| z&pTLnUXs6fHbl(7An-<}um8rOLtvjW8Cp|QP#alQRn<7se1zD)(T@D0q!x;Sc$)ne zzstw$D~9;b{)K%p<>r?;GHfp3tAw$y0oGrCfvchdZYjGJu0yHrUcGwttAt>LKFH0@ zz0>7#HHBIOBYs}NMSMX1^LWO!BkVi|_bsm8qJHE`7X<$2I_b#-Z!DX+6u z!oSgKbR~sodng7NeL3wvax40e+NvKT;icg7ep^_7WCIXNG(M$t_RA2DLY zWwg$&?1=$AU0$7EjU~nHFga8{U;nAtE2^ujS0yGUUatQR=&l_(W5$eli^bAP;SOk^ zB!B@Wof0%}w-LxW4nYjT|{Lk+q2+T3DYxeNJAva^=@fr&IC2p&LzR zC(UvVVkeX$j_5n396uK9;mAOf?p~yezj(XduF$K_H>B&7U}j%$eeSs9jtjSL-8zx> zhJc&f#@!Ztle-(=5-VzqaJaPpHeUsh2}EJowY0me8O*pe!-jQ}0t?bXUARF3HUc03 z*aCr@004llf^HB347eQyq)`Xp!kh&FGhzS7AIxC4Yx?+jdi~%3_1^#9`@`;FE;RGB zfNRHbI@xUYjBlP_A*SnkdODq2^!lwK_rwocW8E#^*QHz{w9Q4P=C{3X0G)GLA&xXoxx>E=Fui>S+dje4+TAq!DkQdDd zfH>+)GiV`gq7!tTUQ-g~+O}OxrJ|T}<-6DN+Y|ENh0<4Fit`|VXo{nFiXjPtT513Q zI?@`dwf=Xc<$2j4{rm0H}!r01!nZXcuLWEroA(Y&IF=E*W!Fi{65GKV1PVr}uu(cpp~8 zds^&w-KnPT(wcZj8278e4zvLh$pATUUbO3i^1uIfbHGFM4pZT%{RRlCLicKUJ znD~^f@G4E=sS|>5g7}(iqAH+W_a6?2?-Ujmjwa(zSf>uu#q$7I`~h5!=K$Dl2d>K# zVSYIT^Z7cWuZNw_BnW?2gw_`7l!fY`MmAT)`%DQ!I9pS3m4!Lo6$Ka!Z3*n^B8%v=Rj+-Mh_%DTJOOIO=_l5{0H**x@Lt5K)!Y- zkv|lGgN~m0y)6^Mp*|AfTYvdCw){*-ZwLo^K#Y$dYszC=x-B7?@N@A3)*N?X-<3in zW=VcuS_rO;N=E|=VlEIp%?D1i**xOt(W7?}45c8Kp*1Q@01`Y9055`g?E7NMArx*4 zAflPXK}yb6hlnE2@_{)yIoU`sl!m-|Rv`kA(%h1t^_YBFfY?oZmow3~z|WEpJDNf` zKMi7T6upPY`9YHaAN_6xi|~0dd?(s4V~3OPbXf#W!kh;v5+p>W2@pijJ|kJtg&Q|+ zG$t5iRG0v{xw-e#ADJgtJlX`(Sy)D~2n3ZDbSg%`#0UraEV099 zf(@UCTEX}#%!=Ww?cja@#dgI0f8>S~1;?mo$gy}91%eM{#@_C_hTdxz95X+=%C+8kx(eO zaV1$Ov7#GPx{4k~0gYs=@$qr34F*Fsg5mQ}+wBYBbUN#k?VKb)UjY>ILu3Gilc5kZ z&%!OZWFb_|g4mMS+ZP{l;>WR)0N;+XV(kf+d>$ud?wDBs$`zjH0#Ou^ot=H={Q2`< z1j84S0NRsqLPA0fdeh&Dd@eb45I|*q*x3TyONjY#a+Ou?EGRWcgw-jQLb+?bi<0o+h6nM&Z_z87*zj)%20wq3B0>0D7(K- zrqX6pj!@f=squ0W1H-6ZAR371C(R1+L=K)Lg*HSQ6kOMMI(B`R8@H zTt#tlag#fQ?S0^jP|LjpgA#aGE~w3JOx#BipqBtL>GrkWuq-1i_kn$#w=Bpl@@#!B{be(VKQj~;|685oZ>3nX|oQ2vx zb7qz~Zdt_Hj~{EFaVNo`1l}oP#E22qsqP;pd%rFM5W#tid~l^#01kX_#P)Yh*zukT z2Y)i+TE76~FZH2wROE+UEg@c+hazhdlF9#%#|St<36*gnQ7Gw#lgRfw%&{YIvJE{K z*(m`_0@z({YQ_YP7#iUHTL4jIBIc;cEwH=KU5W|~Y2U9V!Jq`*DIz^R{dsz`<0Jrd z5rBwIo#cnzpP8_>PB7LyPpBnD+uxIAL zAr!$uL^@n0BtjsPvhqcw=ZSoTAj%|5=gAN?4*vuaf+nJ;BO|xfAsCdvJ4MiPnvux2 z(wkMf0BkK;Gkvk+eG`+7wY8NX39;)FBXZ{XDB6+yv+UWvIQ?rNwly&#=0_6>Mh=7X z=n+cPb`$|+`UDx3v7T(PSZrs`oM}!lC_(Xu)WO{&2*O-`13d(AZuG{9ra?0KHw8z; znbv`DZ7!Rz3zkXt75R>d&%?%g!HD@b5IKI{$Z6gTmdTUgx<+Gfq4LJw3WGYt#KbHf zKYsju1jB#9yD~^gN%5kKujnBF6PG>L7ZI;9Y3c-seA~pi0>ZXm7@VXL_lH0BeraN> zDtVuc^+|y5NPqxeWc&FrklVg3Y)ckHNKIuCbPDn$h16Vy|Mw`a_|pVK zX~-{R@H*xjbWxIgu^u@fe~FKB1*9G71z^bep;HIgR<3}Mkx^m!LAIf+tgMtxn>M{i zFqB5g3mG)Y3#5yzx(Fa+%P+XpJ&+P;RR~ZvKjaVS567O}5G|Gpvn5)ru~T)~j9@4Y z-j#s1>Xa}B^5{ob0VoPAD|~R&&qz&}CbB6Hlm!bAt4aU`TM9O!U{n}f=gz_Ha^1EN zcDvnu>eQ*>#BmLR@-}SPuzP9xIg12k2@#Eg>1agiFMbLntBeK~qSddk&Yp?F`1s<_QKY#axP1BYf)*`W+)psb;9Ut; ztXT0V&9XP?Er6ViEi1iw+~83z*zuAHmp=<+qLJn22Qwkl*AFQ_`yt(16Zy*dp<5T& z*RO+Mmd4(m3c<^jQBhHmBS(&WoM4c_I~m*qJWWa+W&!j}!Y;}Inc==T78K-BIlAXn z6B2*)mo27p!DVZpKk|nRhU4G?h&G$YB2XoCV$`Tn&k+nVN?y=bZO@)P^+Mr zNc1X&N^GSHz>Didymgf~ZVU>*&JU&Or&{u5oP0k>U*Z*P$uecK>PTQ$p$T)stXZ?( z;9^Hffa;W`-x5VJxv~UslK{>Q-bftn$Mb%+8%?$U1{$(Oe-prWf0c@m6Ve>6^XF7o zNST?LX$u!F{GeO`XcGD!36NeXGZ6sUr?%mEGZQv7k`t{qY^J$U(obYROhi?JdItNJ z&8h{UW-N2@;>DkoSBlo4SwpJee?I!i2ZW6@U_i7d2uRD^mcD02VapT5~AG`0q`>GQR41@gQ(MKO;Rqi358cG+4dJ4egx0-SU;a?76QZ+(( z937)|98FD@W%@J}MnzS~Dw)k@b8p|i{deN{Ugnx$U>M{V#q)`~&YnHnoczk7rvS!z zm+)7MX+&ZbFUat~%@~OM-+zPi#0j{a4h?prTmjKSmwnl?Wo?*a#aWOnz?wB{KB90* z)H4aUnREyX-{`{DQ*}#{bj<7B6ZUNp5c4YbNeLDpDJdx>G&J;!n+d?%9X)#Vs85M_ zKV77c0F-=t57~fK4S)4$QJ%%(WKBoy0?0n!t_^I97ePo#VbUvQQU21UOGo|v{k@oD zg7S9fop;u(RjbzHG&Y@~rvL-{EXSJq%{{ui7r)jCQ-Ay&i#T#>%VQ1C9!tq--V~Ne z6X3dfrBa`kqg6)i-Me?rGtWHp~SvqJ2 z)S0eYQ3UY4rn|60N>F}pepNG@OF2!ntGJ4Wv~$FF?=(o ze>(!J8?@ka$_gO+eD|)fZ`#ORtTvz5fGFC!cJ2BBb9*!Ymj$@9Ns}heQqowYR~2NM zdl6%d6I3VR(2(g|Ih7Hz`QN88sOM75`Jgw}&{$(rJ(D!9;D>Wtw}N%<926!dXkT2{ z02=LJ|Ni|eyu7@gXKrsLfT1RBba-g@?%i!^FlW`P4`@eGF#DsvH|>I1pnpCegw~Uf z@HJ%{y5fl7-~Z7v982Hmh!~$1u#6oI*M$pk7Zz%?8}0T(i!ipCGiP>YPHRe=p)^Wf z$S7`;@9WmB+v`-jo}f3;trPAzY8Q_pu%=#fxx%3%!$=d;7MS+kNVFfjPbR-y1kAsJ zrVB1(#)2a-A2U|3k#7rD8)(RvBMD4kf@(-|gu9k@`MnkrsH2q5x1 zthkOvC(@9d>nJ8cN75B={rdH|?%lh8!(2+7s@&vih(;98pF4MMI^_blE&?ccI^m}0 zF!3RauVUifBwR_eLJ-BOCSLw(QV~68HS?i|9;&6L`cdqP36D_w{4Le7(N!S8Er~ag zIK#Kb;!sR3Y%T%Nfq1o@@3m{!`0oFKIh45caFdfUZM6;`K0KbvDWU2GV4o+@vB%md zY`BmGv(=^Ie}d|J_mgdlt5&U=OwDa=^~qP+hFG~rJ9Oyq5q)pq6e(L}0?0n!cKJ2T zJ(`MyY`gwGuks<>)2C0L3knMQl(~~QBrY{>)oHtzj?dDAyWR8h@(T19Kq9{RqAM7) zD*@+|EpQ2<{&u4h^aV!?ea)*karPW@r*!vg$^|dI^wP^GPo7*tgQcR|5Wr)*F;YE0 zcykQ)-pGN)sjts#_%dG6)~#DtG-%M^746=yY(;kyo}%XL>)6=X^MW8~5@6dEGZCuJ zZkYV8t8ZZS*$iYBI5qgZ{?pfs#SY{jv=#$-LEYnKm72+ zji|mILz&+0QHT=q?1lIvGM-4jq)2=-^7+sm@i>*34`-qNKCcyYLptc0s`13l3u+Rt z=7cO;(JQaK@;WUrEFjMlJpCuC*V3)z>9Jta{YeN0+fx4xG1zf68~G#>wA+pDzMqqm zBdlM)esQBljb4|nM_uC8ZbkQVRrJLdUwm-n$dL`SZs7Jb5>oS=;2M6)z9dZ9pM*^p zvyetVE7EQ^|KsKwVDt=vXmPe}+qSK*M$FnVYCAleQEdjbZM#uzy_ZaF-H%D8J@34k z-Ps-Nsw&s>8tt5`l0ejn=mxIm|B?;{GUI;Gw{PDp^tx>PZ6iR*WV)H>;4fb*(Gcao zLCSt}QBhG2{l4f#2mGH&_5%!Pf&bmn>6MF{HJjj$#0c8WR6gRN*IPj4<>fr_cu*H# z`2B7%f}U(Ro@Hfa+1s~oUxAJJg|-_2HIkQW*REZapP!#g?4kp`Zd3l3eMGRnH3dOi zV={t}CiK8Rkt3>BF)*>c{3-JLEz!}@Da1_dU-tQ^$_ShV9svOXF>P&a^YL2X9ri+E zxoU;W0Tve4@7C7VE<|lNR@{!2s+IqxJF*13;Vc|Fefsp_hzgJJl%VMbAO>9BI*L~W zMj*EzMvTO&8n^TFK+PQN0`^BXuowYbJGv|GYuaj{tV}H+1AHH6{0?-ql@OC)GyihK zRVO3pfK9MG9q_-mhlfWDdWZ<454<4r);=Hak0L>eraxQZXT2#NDP9- zP_StP^%5Ux26p5OLUkx~=+L1G>Izi=d4b0{EOUZasp6U0WjcW}p?$o1(4avIWDTRt zBD3>sMA+SGNGm`~Vt(A3er(e_0jXHrsI*j>VHl zh~y8{)YL3QR9|jlViHanMOLd%W<9WIE^+4QN^R5z;M@pOS64SQK0ZEk>eQ)Y@lsbO z-r7HX^XARR>?YvK5M})fg@2goF!URoI}y0nlfd|*tod3EcEPzQYMkjMr73$ z*|mate&Uu)#1K4D_<#d(bd5%2SvYOlwDDbCU3F*z*omH+YcR+o_~n;u@1{|xM9yAO z?3er@VB*j6clf(d_@lFD&tBUJLZQ5lW8~%K6>?0DO<M8<9=i&6l%Y`wU?OwghAFPczAdkQXbi$B1KUuuwnKi`19$fw?>N=eqh8I}>`g{eCfNtOW_Gt_@pT--A!IZ>FJaevkT@go zvZ9gTf&`J55|}xaL6#?5J6uV6?Z;@4)Efi8c|qc8Y)H-7sCjxPW_z-lU;4X)S!QNtW@ct)W?tLFcf)2J@0giMW?@&g@_xo;?6lfmd!S`` zWmUwB7B=bYCsd^*$M<$5M{*=bawJD`5RtK&q)c`u<3$_5!`MZz1>$k;FDalzVHCIU zfazd1myaH0e8c+i2K?m@HKF}}p$!i_*nG*R#8iHL-x1C5Q-WhAs54IT!|K^+(*86t#l2L$07wn2=HK{UZHJEsndoK{LRQj^WRxkT&JP3vbK{j4#l?AW;XlUq zKG2hht`mvah_M@i9BsU_g^UWKqc6AgmaPC>Rch-|X-2|64&p!4^4@2xkVEGBE@s;(^V2aKYP1zGEh0 zqRawe@nlq*lvQm~Uad*Rjf%=z6xDYr>gdXtEt*xyrp8bfZ&>pfTi-4T0k!@C$Nu|w-x(76c6~619hzKGo zf`|^t%F3FusUi{q{Tj@PtAO~#OG`^VT5A?hvAr=knFZv8U8xd|)bH1BI#C2Il675O zT?Z~-zI>BJB)QMe!K}Cm2>$?M<;s;FrPR)@JC7_awN#L-@DKp}*ow$|@lT??2v41G zmS(S_>UKrz8cfQDP%^5-fkvR9&ZPD}O-i{VBHkkJEJg&_EiEn05zfZUfqn#L#6>`S zq8S+(21K+qu4A>U;$e~ayGggKB(?5YQrWkG+ewu?Pr7wASphFZL=(b^c&mZZ7IBCg z3Ru+Kqv@L_1=r!6Xn1Jj*XueJg-mxuq_w7@p`o=%s<%r`O&zs5a|mb-m^SVLvMs~- z3(h~)-Go__<^6_K?mc8KI)=;zhexycSWsL!W~L{w3ql`THcVQsh*f!Yq`N~teNb+DtOwo z+syjT5c>L9^m=sV2P}q76p$^fEjbYmc0cK^Lb6nd!&(dlqzLzg_PNhTdxO31_0fJ0 z_~@i({dE7AL0Z>nA~Zz{DFQ-a&AE@$iCB>AVy@&vT5CdosI0uae0LzfUAzP&;z4p| zAawl(nn~k37>iyHDlQho79e45*{P(~zbyHY;**V;fSjb5Dtz zkrLJ-Ti%KGXph4yKRi7z0V8>Iuzj|v_p-Cg2J ze#BxRz=<#Sa=HdIEhr4rWp4)PpoiFjqQlya9|qtq(cz3MFP_17R%P6&wk-ET875qp z=H%r38Kk_OT4Vs;#$G`A`S}Ktu<1&v>xjs24_)yQkPH0xY*J-!knXSJrlotOy=>e1 zG`;wr5FPedw21acID2wokk<}HIT25XcqEytFDG4>N>-p}v-?LQtwdyp;k7`Y zW{0Ch#M{I1e08}=5r+$LX>XtMzvQk)5~B*!FhQh-n|_ z^Upsws;a8yXsutcPgTTIK9!2Xk7$#oD!A-su_z58IfTVw?e&!*DG?}wD-LmM8wUC-9ap?nTp8f70n#EzvRROiMv>0E1{j~=NI}FF{A`)9qWC7X3 z8c&O?xsr6lBF>K-VQus!t+_R3ba;~KiyPkG;6mK7zVP(`we?0P(=A?wK9Fr+wY)F+ zOth11i3<^2$eWj+f9(0!KRNaAM|ys^*L}X}AZ0A%!PtEE*=HV1wEaLS^}e%gPj~^@ z9cl6L|6exJp6y&V;t-Gx2|Zy)1@-Ql5FPm7hLZmHr~RDs5K(kBX735{5H*98%=H02Xr|Let zN~~3~n{>~)R)s%&w}W?Pw~n2+JKN9W12Q)lAM6MSmcV~30@74X&YU?l5GQ?y|ExD+ zvsCE>O4x@-AZCXvMQw4Vhz-C-(?7@VbmjMf6j~&|W)QN?F>8xZvII)ba&Vgd+mycp zY5wN}*W%0x#6q-x2GYRuAXy0{_Q{MdUc3s}y#IoCoxzXOcUyj63ka6Le=K6nnl&28 zBAsDi{sOiV5U@%Q2r61mrAd>b73?%d_OtNM*uoOaKK=^8+=1pZ?YLxv2|1P2FK!T&*b z-p4+QCQ$4yS9&(ylTQ3>pdE#MX=~o3v9qWzop#gH^QoQ`x7!N0#_`eF0mQPA$j~qx za3YC#8jt{(9yz@?PwTmOxc*EWtvDVF#L;r3_s|ka>p>u%+3&m+e@8-ul#oY4py=2H z^8fSC2N2@d8Zcmh1`sU4;s;i)T&bNfVS*jx8vS_!QnWrwB#`K??)34dJ!O4qOKu5i zq{xHM0Q)NX(!-&iAYRJdWca#<^k7IcdNFni#b3Eh#@HBfEf(-zotQ+Gnn;S|+at;S zj+_}8nD0|V_UVxYS}_)tiz78JR7A;mv>i2j;XHlkRF z%0)tZ;~0{rf`bMP(g4E0;a}gpapQ)jTD59g%v}G%sk>M>R3d@c61@2dx4P1bBj0bjcJgPwvs-H8#>y(>j;-9ibkU#n{S z2lPUL7|ZggTD7VU>P0OeBnSSJEN0D`rCGIVl`~j5Su7kXl|Y#ANrG!X27^Hd;SpD!gTr*fGGY}^=%GW0d~n$vJ$h6F z2uXqeB#DtDM`~-=u5FJBeNY#?GD*cJUk#~UrHv>_0D?QR=|1!F?PbrocLK!|1;=KZZV(eu4EvP2zF~6AB`AB zv8PX&;v*u(5D2<2f@tsv;@~{(+qc&NLQ+WbMHb7KD_4$Jv(&^7K2QUJZuJl?knLeD zfut5GxOr2Gm_3u?Zr(tDEnW%Mh+7-T|)^kUPxpkp9QQF3P)ACor8meX6n?b zT0oFONa{sYp0)k^_jiU`ZKAaIrQ|GZUj>yC$UN=$@uJY)Jt^kk0SMUwnM9v6oqu=! z{P~%U8Z}CTu7vJ`TN$NFmC_=R&H+ETP1OA;oj@G$qG_sL0!iH(3Y6jF$5HIrvy>PU z^I3?5k##~DXWxt&GxAohTv-bUQvAOcx^?TOVfTeEmO=o1UYfouxf1<)E-KNLIp9?< zfut5Gq;*S*m_L`|A3Wf;$R`c7Q4b>M->u548a_N^o0>4M*Kyf$B!S^ zLm>6svinj}FU*w-eWqSm9`zGQv_|~M@ln*K4ICeR%Cz6;@9+N_F>!rxLff}*UkxCb zLy*2OY0@NZu3Wk7p?z+`Q?P0x5QB?;cY1l#u412_Ks%8IKK{|058O8J_M))CgDB?E zACwptYI73t*&~pS#t3v<2nro~_wKC&1ak<|7bZ`hY}cYi3l}s{2XXUMT?7L5nL6aB z-Sx7PV>=3r`$5W66%BN$hMun0!DCH5o}*S&+?RG2@}+ZC^z_Ef!*&D`>qku+Q}~4O z6nFIs+9HULh-@kEFrGMZVvMV+YwBUchS>pvIRxno0|ySY8#881I>=XNTMnQVDk$68 zkgLbC5QiJAex(d_qpml-Z0JdE-90GK!<_;>O^^Kim8&N`|Bc_XF?l3ue=y>s$fb)Z z{^3KLv_-%l0uSiixpPaQ56S=oV`tF~El(8Doe}bnhDLt$X ztcnklgAY!z2L#EO{idkzqq28#zvik&#R!*_JTeym|8^ z2$Vys)#?!5XaGTkVE%;L_8Bu~w1bE~4^lWWg;y?t_L&g2){XA=^`e8-Br*#taH4`9 zt_kGvQaE{}ZVj@Lh#Au<{_$hk=I5~n=4#KLJqu;YlEt1gv1GHbe*OAs^W@264>4JB z5a_nj2^786l^&1uq~ky8O?@yKh{NRDQ`kTc>wAghQ6(T^gQWHmTOzA_Vh( zxbpe(<#TA=y0sP%LW+?C5#69`@T*9#Ub_yz=WLa|ww zY$Y0QOouGY329qxeDRG^2$To{@sQx1J|;Qo)_gWB1{UCApqKgy#6V*<>i4x)69}t; z0~Rk{T%WlCFQo$n5rX*>bl^EnbH>UuZes*0g+R}?ETW@7qE%4HAi67?VOJqvr0pVs z)Nh3%X3mg8I1$GVN(w z9s`19+Y;!{FNHur#OzsC6A1mVphb%o{e&4o=Vr~C*#UwG!TgC{ueZY_hV${`$LoMV zig96cPre}ECD*9Ou-!E4*U_}8NOPOIutyEPAQ!f2qZ|YZK#8XTxZ&dF=4J;7A_Vg% z9v&Wcg$fmN_Ve?r!JT-e5NO`X=j5~E7J09{P0i+Cpn<*C(~{bKX-k2|Y>{LSqOaBQ z`7(m&mdz>t&TXp+gk9jT(4F|MaN)u(zyA8G9UzDh%%9Y+Utfo=t1}i!RYDs77-_ts z2qcn-A6v~jL!;X)qE(f;n1X5eZ`f2M8hr^Cz`x)zYC7?Yv^eic%GW1HBGiP?jxU(G~h!D)5z(ncL?{I>stRUo!=ao(% z(IPcobcK2j*-7*352a0oo5|wVj+8NQ6IDGV5`|}NT2a)RRcNffX7b1}rApVXUAqch zba9!Ez+uJ)bM*u;DVEzObud;J$ z*|KFW2#GeTg+NktoZv^xtM`VMS)7SI(lyk8;&mgF)VKg4`<@Yy+_)J z&ctpkm(PRCi8%r!dub2{IsNwAZ`rWoW3DJDQyYN{%WlxbMl)oKiS`#W(8cOL^znK)VZ@ z1odpKUfYsH;s9dX)>vS*h~l3Y`fkUL9TQOA$^1bCDqp_50|M!^P@%QN zk3^`7KpY3nyFe2f&7#c(n|_w5jE4<;Y**K59A_3 zx8*cm{ey*KTHduYMQz-`an2`S0|NuXDg1Q^4QpyN8W-jUAecjtzJL$vQsF{#wFs6g zY!^3kD4jr!7GI@V4aa^tqs?eTL@bz3ad+-;oby>`<6%JPq@m%F@fH%!t*sy_?S@?d*QrEO-S_FmMg%3 zBx1eGT%_+g3l=Qs!rZV(AY59Ar#aeAhXPVN^u&VH;KI1Ux~Whz^{#h8?<%xkUy44k zpW_@QEaT*{<}idZ-NA+Q?>V6bEM6LzD+ff~>DRAcUmq=v`(jH&$pjkHW+8Xy)kh$4 z?w$7n`THxP?k4uYuqX2g+FA{rot@J&Czu-_M4${AGT3L!mMtCnAH`tC4yb`Z<6F+7 zE&1gk(1v2IXomX)n$Ub6EvwmEl#R$oAaMkzDcTRSu$k=EO>RFZX>%(N6cR?7k2Yw~ zpbT>%efsqFA50)@q|)I_sa;%LvSVY}1pHvU(g`%O{ZiC^npjO9Cd>F{bE)y-E5sG; zRx?l2*j5WnvJw6ah-3|cOmU8$!bT1^?M94>QsVAtUaF9=dGqGkus%7M3(ScRB@ka) z~h!fv7u7vJn(3;nqHE(Kx^|i^vtru^;-e0@nIk zo`=VV-b?6-R)Xcpz+CuP0%>sBBl1p*ls+FuTc_iP6BSG#xSQ)VuK8TZI2u{uzM9bR zjw@KGmRz3=mgyET!G?5QO-ngG$}h%8N#E)K8(2;4hZ{GPyP;l^z6S&Zyo38%g!DZ> za{ycr0ZV+OgGoozPBHB(p z&ZMO^OM zB39oY60BQ05@J?p}nFBB$BAhn1eLU zV`B2{4G?E-*$y;WKaDy}Jdz^fFuZ|Q>+aHg%&y&f70I`cR26BY%~NzA?)T`?qamL; zpS|e5QhI4w2a9w*6H>WCSpB;gB5ScXrdR@r?xo4X%hVO_g=<7JT*uM;ABF%!X`1IG z8rFFwbwaw1CMPuT3sZuEL|lOjkjZbxfISiE1$V_?n@F(!w98E zgEbenG&DtFk*yGY4FvLjZ*`ily+bpP{Yf`oL{MyeqT1b+bR#|QK@1o^e0VEPm|%U< zi@~#81k$l04b032(S2E{)YfBONGvRVu>=z1oDut<(8-4(6d9Xfa#t!Rj0B=^eruH~ zRm$@j z6weX+wBWPlGsam$08Ia!P?bD~vPgwhLOAM&hP9QI2 zWi1frOg{XKu02Ezx9z=As`h3>g?!~6@}j%j(|D> zu1wVgG9WDjX#4EiCL!PP<_&aT=a&RKBk1wDKlMG1+byZS0%gwCDN+kA?h zKP_;M1%a%5rZ%-c5-arA_%q_6+(`S1Z{NOs95f`+Y!;PFAa9`Qn%gwd?-~8?Nhrm{ zOS8zY#&yMD=iwBbT}eJeK1)7R)7gHSO9wcExtZ`^AU{^))c~V+^G#Z9;mQ`Dc|m$B zEsif*7JYX~Zh^16+ z7z4S=k-gj`+KF0BqgA(4Tt((M=l6@R5$Blp1%8d>0gSw=<=C-fH=8$a?g^_?kk1Zh zXgO`SWk3z2;~)%aU~#O&?~ZurmE7^r{Wnq6cEdgL{>UJiWfpT!Jf~YPBUol32FPC} z&^h4ye!UqzdUPM0R|z0DpPh{ZXUi=Pz!?@O6SA`WC@_|X9AL`JmoK9dWSr;bSY_GS zm)r#>?+_Iz~B21ydWw8USV(Dx^=(dvjKAAtmMzSth7Q}zzN_2BNwHLVl4V6 zl*x~xQvFU&60tkG_a>4K+ztX^UeVeMuQ{`#2XCV!?T7!!1216Qy?gfw+;RC=oJlc0 z8$KgI6w1qQeXNa#fQ$&x@*$`$fuThAbGT$B5x^u##A6%+V+Oy@ul;9r{z0C12qpfH zy>o!EEQ!MKddIffwmlBkt+j0U9#P&H zXQ5H{Ecc{O?^z62SETyTs=>D~7S7Lt^GkT~cSCv&KZ#D1Fj2(JLwn{PB#+Fq35-+X z9=I1_PgaeTc_LteY_i(323c|z!SqQVIiEn;j-40%oG_scK8{Hn&r#@Sd~ z#bPbyk(%o(c!hXH%Z&`Bja{9T46F_Ra952wZK>omIlN0HsbU@Ui1 z$JYY&?chIqo!s*yk<_!qC}5Q-b^*fy@iR|6v9r~}ojnFIJO$k-4@J=8{;IC7u69h* zOP+rE>5p(e3Z|eg>sMQkLm!z+GGWAzF24BUOWWJqJ8;HUi;WnGm6!$WR*O5QC+;YI z>UpUUB{n@a%itfgY~sX;^~BP)zMerg-4}t8mQ^N_aZ(ZV=bn4+%d4-x`i5P0*<~9r zXb=lA5gRcISc%!H5k%FOT4^*On4e>qQ_Nifv=aVZmY0{;viIJ5Z%+yCjn6#u%;$=A zJE`lwUz(YzZ|z>w=Iuz^HP>8o6NhGdy5qJ$d#SpPScpl$MvMk5Q&J}aY2>{=juSTx zv7Kiag#VY*cD9iek@T_=%7 ztE63Pf;f@W=-cJ_o=%;Re5|sep<86Dv1s9w{4$PDEF8T)R?Qh-uN+vd~-=$NJ zBQOk-MyBb7!T+X(+&8ZA{PWL0ch_Ba-LAgAevULiPq~aEZPF;M(wuf(t+M!}PQpl@ z%4wLf>PW?aU#7^XC2d`Gb@ePahN{amX#DU1iEQt@@x~k9bK+ABrKh{En@HM7{eNL5 zBgHIq^G*HVpRI1@)r7+TH~y^ev83K5#o|HNgi^eHRaMn2*BY*=GHH+&X-bngL!x7x zgpmYyWY7zySk%bL?YrK}bx3ORQexD)$tIg@bnw9k?|05Q=bU-VEw|iFqW=qAcOSE{ zKcR^Kt!I~X+tjtf#%l#x0;A+`rZbEG_j>ee3@7N^>rwwANv>B0MIZa=g%@7<7=7ji znD=f*IZBUHvH2`vg4YPAyaSVc z$W{0W*#e(A{Wm(xhgeDPP$=-4Uwhwu_dSMZ_;!3%ml0YxmY=zYKil6y`OFN~QseJ- z4Ph-MuFW;N*0AO=Y0B~H{mVEBBdr`Pl7Zpm>KhpwHfCz5f6_5o6gIDne|z0ID@eGi z;T^0-3hkN;3JTgvOG{_)X8v5-ovh91Vr)o4`o>QGz0Nx8bon{1pt=9IV-~0m1VIpm ztlM{YjqCj{m#*+)eSqiyFS4z$H7Z}lC{{U?OF4~2jpZz(+Uh~Qs3&^6r`MX^6C(=3 z*@KWEV&tYyOt)P1zcAvzjlU?0+O~Z3bidLx&8V(xz3h=T<#8NO?B!3*wqyoVA)9>U zs~E*92X{3+K=!e)(a=noIVM8ztJ>*U_VYWGfnaK+(Gl~sSUGkD|j9d02_Xyw- zp5PH4@VO)Ah8&mVtf-NB!GxLS1Ox>!BpbrYp)g9Q96Xl~{s@I!F|>kUPFzr4R1i60 ghqt%9Sk6%}a~ch-AGh)%mH+?%07*qoM6N<$g3o+kWdHyG diff --git a/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index d1eb9b78cf195a355c8354f38c22999778ceb52a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11743 zcmX9^Wmp|Ox19qAhXTdj-6`&F#T|+lmjcE8;85Ht?(S0Dix+p-;x5JE@_skZ%w+!T z9o;J{D+yOtltw|sM+5)>MOH>a^*!(Y?}dkb|EjLq!T|tNk*tKMx;yA(2_!+OLO6KI z{i%(nO%F$eT=M`;gfclEdh^3-rXc7*1Td2P0Mwk3f-nIw+5jrCJRsorkAVCmbMy1I zKm0or&MWIS_)TvlB;twY|K9x0-^|B^CG)2&Iq;*T{y!^PIrY3ir<7B|r1W-YJuB_; z(-|gb$<3hG-ARoB%n}=T8$bqii%(2f>;tU+bbpMnT^Kj@O+YSxOkmYgyKzTexI$-& zCSjFLT)6SdN{kUkElr^LEq9P{ntPat(a1s+BH>Y+`%#!3LbzOO{Hc>`gYe5(@#F$e zq#`zaJz1mOFD?h7zDmY)iQR5vw6z1^B(rOEA5*2EnHn6UDt=RnEn-N6yTxIQc?DdaXpRL#6_{Or*Vg1Pu7t}LT_Y5fbXc0b5;YDIIN?H{q}r%j-|q(}R^06qQ!#Vl(yFEMs)_&7zm_g$CRNi*eH zOP~T{FT=At*bYL|K89ZJ1O4BMt;*(c%D{R_$roSZ9v%~dOH*c zBwEB|{LW}I*!ceE@ziE;brfl5sZek6%_pfG3&?imqsz&UUev}5xQ_|SC-7}0T{qv} zU8>ygSZ)z>bzp6Xl9iZB*R)6@?c|}~ctP~SG_oD{krtd54@$1v^W(-m&X@W~W7b3n zME+zZEQTng_La*^{{E70CqFVV#AXN79{KHhmhLDT59E!mTadd#+W|Pg2|`u87aWUg zzD}V=_HJ@}S>UZo)kzv_ZSOLD`Y$+e#Py_HA&pV0hG8K=P6hz=jj?*zizZaKwRSq(v zYnm@mfNw4=EG(BA%QlBk5{>a|bDSx>Ixa^?YpW+xp`~bdKKk(hKky7S^{XZHTcRjE z46QwNL)l0~g)YnYV@^y?1odTbS}P^rBh{(_uQeGui>^U6_l9}>X(gr=y!&l-PJ_-W zRF3^Ot29?0NQgu`Nvo!&4Jc_LC-;{^&H^);Le97Y3H$Q03vCOaf+xI!+`;X7)po;83uz4x3T>1{h$jwb`SxBhdItPj!ppt z2*1i98!9kQU-u^)3Kjo`g;+T4HNoEl?*_R|a*f~y;$QZ?Chm zlJ?@i7=tE08S(N6AV&TY3XjJ|XGfM)m{N*IfO^!p_2Q$ar;j#!~-lk(8+Jk>;MGn92!*GZhSx)3f!xH8uQe12=Yb@uN;@_)p(L#%IzW z8-WvZcCzra9!1Zj+C01eq&mKysCkg3rKfL@RbLFs2ZMV2Ot1iEHv{Nb<9XYde4^!5 zSqf-KBPn#<8R|WU+n=BJt6C?`+v0F%uk+#iuiv*;{u0kN{HQ6DnZ-m(#zi8_7gSV` zWwW-ivHx4ZlQ?Ce81(f6px?hQ1H1^Vn(KPhoUyRmn>OBH5B*}i_hvT-l&f!JN8BgE z@AlioLwe@LlgRD6KUyVTgGzmK+Sv1=AZ^fei&XzS$>oH%=p{>>%fWhdKf4L#u>}xOW&-=(qjS&ZJn3Z0q-TgW&R` ziqsMelRSEJ=teQuZfBJM-FX*si9WE|Z7f9~qs*5&XcDH2?tGZ_4jw}G5jYO?7h?al zZ_@huYg)d5wj*>m_;CPhxYT^`#WUQjNg4;3?`0q*u;`;{OofHGJS`|XqvDH`nr;40 zH4l~NF_6tgQ@7u=76xN%LjlAN$A^bYqJrVr1}}%^adljQ7AF#)KTitnS&-%1O{d4x)wr(=HVbN=o0In zFHox38v9me>0&o>wu&`~S1%hAwqJA5ly7O#z-xg6Z6YYb%p)%##9(V{Yu}w$z=JUv zB=av+$cpgpqC@a2EHK}W88%{Hg3c|cz>M(YZo-ppQ4x)Yklu=G9Y&rc-xMoMoWy0)*yitpwa>vPYLF3AD{P(FR7sRZ!!3fqWjceUqERC59e?50i8Ay) z6t#MAT3XsskwW?}3XhY)pGz2t@!D)4K;KU^HwJB6;f3kLSEBch@E|dDTPnFl?bO-o zDIGwY{APT=D3{J!0Wa&Mm>|n39x^gA5;OeOdk)cNmcNJav01MwAH?IVkqrhIiOXrd z6ZmHHuRNdixBE*6s8sue{QKW5Kdu~x;-5HDdMEeZX=u=^PfFCk)wHycF0k59iU~HJ%(y3CqJWxbC~T# zzYN`%;-J)fYQTu86EywHOZ&>CQ$~)#kkHUThSqsiW!7S<%i)bR9F^T>(AA_LSqy$aAJzk(C%-7OKX2u|uUo*w%?yXT=<) zAL_>6;4f+g`9=#IxEJ;WmGty@RMP=6bfVKi@z2r?Zm1@X<_|yg6+Mig&yXZuKK1ha z)A}SlcnzmEvu$q%vm%$tN>fZ~IeBd&s7tS%2xks()&Bypwgvr@d=;#Qi zl(v508}0}aPdq)2#9)DP(2DQ(Fu8oS(|&2emJ!SOyJ=!x2ezl=x>OO>Yl%U{9<3tO z)z^2CDaCW=BLai3g@uLuW%fBs@BQ)-xNC^0y)t3H9!hjGqZjx@fhzKLmqNQ=bA@bovWd8(3f;CyuK zbGm6>+yQq_-EJkj;!|gqBAdcC`b}J%s=i8h^0LVk}+D8+ziS`SFECJdE!D=+G#R0 zny%pwp*zMe?Ok&KZ8KL?Bo>y25Uv3&$Ndb|PzPgF!bL2|=++<^v_By%+M1!c0pndp zA8t09+#x|sRV~GVph>jx7A#ChM|X@!mVCd)QV9!Cb$+^=2*U@EqQ@Xy3=QY`+ntzy z7Pa0wf>43-^F4wsgHB{2g?d}|3T$&sY;1EPpPMOyIIScLS=k;5VHm7X;oI9=>7zc9 za*&aija&`JRme4#H{n5jM-6P`8oFzU`kZ-Jzn&Q`3g87k;Q{ z9HM{oLsCV2l$XbqKf6OYNZD0OP8~!#C&=rJJA2(v$tavny7fk24BR~Qz@3XbyL~Cc zm&hsr@w$Xg*6(TvvE#}2yh4Wx7la#ByM=WAv*!frSVqJ603@Y9kn6iQn!bOAETXTR zrT0#5ugg;~kU6h@GY5qyr0KtP@@kP~qP)Dk5heJT*0`A8?-SC~+tMG!m^5LHZf8H| ze*mo3;|^(C>0p8vipzA!GcOgukU78o(+g9x0iUHrfl@2{cmAil;e9;@Eshc>_Q7xV zAG?O%a4{BWNbW0OB0>%7e(2eBnh>$Ejr;=K3#n)ng|-SGc=~Z#ZikhL0G4~gKYK}% z%(^PBg)87idR$7sVgQ;)>O4Ykm~~w&-$rwtBQk#I3XxC5yAAjC*#$YCW;aBqwP+9h zRVs&4e!H1{CjsSm%jH{X{BT0m+|fb|Isv_;ZEK1i-PV7XP=?yTJvB~{w>mehZWX)H ze{^SOXXAq}h!@O{Q@FZ%dV$~thrpFCZvpP+wzhueDz;T}nT&Rxn`6mLeS5^8X`f7$ zs4d3TIkSe98|RnG+qCX9Z-m>iu#j)M%n{J)>luh>k1emkXy}6DKjY6<$5|ghHQ`k? z+1Z8%0We6`&}~D|oiuQe*EgDP*8_cqsT&P)K#+yk3;3Q~Si2dbYS1x4`&Zed-FLCk zCIz}?6!l8pJQGN*QGt&Q7hJ$VzU@jzKx?lcIAz%>!Pu-1b9T=0vtN;`V5`uD1?4-0 zX7M=oqAbFM_?vxjVVPC-yWfJ7$zjX@-pFpc<`Ezu>}=*ogp0~B{2KHzp>F$2H~dtr1DrA=jbp(O-dyiaOtasvl#6Hg&Uqaj6pZNf%@Rzhz@uK z)(Bln-5$+34reWa@8LLR$20jqUlBxr7pdVP@Eem!q3e~|V1K&;jPB-2lm72=9@hg$ zAC0qgk^kO?C;4yn1j13gUNv^38lvRBEg3H)ZrpUp0JgzEZ?;oK$Zh;4@Dvij?^=(! z8MRoId9dN-+UIJ=gu)gnJ5XuR(MrT>%ZUDJ4%P0sGr;)lw3otSxLl6wH@m7Cwi4*6 zi#Z#*gyG!69HYY3l;550Ii2svJK6W^pL4Z_(v_W&5)D?wjd2!w^P9L*MBrNUsV6*@ zrx%5=%$*z>U+>f1ISCYUJtukjvV=AoDH01gkxldc$UHn6>p1&ZEk+XIfl%PH0?v-o z&BZr;x&k%s%kv5rqqZ#0z<3ftxe2*RY4v~)Btj>M$mx<0m3%3)kaA{vdh61Qw-tQQ z#~9~4Bw=2;6gKX(#i^AleI^vmhlifDxx|m?#*R?%pMMW0(5$@=g$^g^Xs$ezkd(hE z+3g2-F&Q4MZzrsC?W8c^4b3ex(Dw|~ZOKLc^snf?FEs2fMLQlSWn*K*jT3-Hh8``0 z@6M08c-xHlesT6zPY(J817&m?CX6~jSVjLf{8ke+d9fUGz?M&Yxy%`nsF174N`82J zeB6gS4#NmNL+JM)aM6cDMws(=Bx!U^oOK}#{?9ZAXcF)`nQeCgZOis31c_eXpwr-od`)U$y$q2-2#!1oOXHBFNbha@a31S2GZ zCPnheVgPw>Ud3<82<#Uq#uy@guJU29vhLuL`>-++tlxJjDv?e5o#64NA3WWGT-9xd z9}nsqqw3%x7^G$oSAU5hX1*mBpOOGS3^0$&5hi?Wq6z}*;0}GIweX5Hb-M_phB+kD zP%tk9HAxm?nk*6mzE770D^n`}%a@MfvbY$uP=|`-zfV08c(yz_`P$Gjj9X6H6Yq$|IOhml~)wp#@f0DM%)93V6*Z?JG zBU8l^Yi}^qAq?1EUFyMDBQZL~Fr-^UU2tgo`1Z*#p@~RzX)IXQ_OUN(RcW=NZOVyz z5#7T^PbJ10>3~H@NIf5K4uwmpMc!Utgk?$3zzciaNEGB_Xd=HS`*AGwkF6Redd;SG zPM-YWj_VbH#lk|DfQR50k@JB_yct66fi>(hgziCUGp;_t8FCPsE5XZ<*|O;JvQdzv z?B-n{b6|~g!~*Z#|2L+(?F)sokn;{!?D zyuC3rd@NYW-Pze0o|(BO{@Tn_^M^r|y_8CXD;i2b z$7x;@p48;`vs1%XsO*&mlDZ??P3;l`tCw|29Dt%r|L1O``wKfL5Y-|c( zqHgALXD|k;4>9@8xDHFqHfSgN8V(MQ1zr5|mjr+wD^aHufYf-}?@KZ!f%)`;F`m^k zmUzgHIk6&ddA{C3+|623kqr2q&z0kt(_KG2Jt?8tsMy8S5z0en758G-JMBNEMOCp^|wGl-oViTifd@40?^av<*_jn>bXZk>9U zxi2-0EWn^07oEN%TvzEw$-4R;s2ODllfUe~sQX{JySq1-X!eH*bSJ}avi~JLWm;G% za|;x*-}v<6(s^wu*X~Hfw_&YGvEJs!;yPU*`-X(4e`h^TKy8F`5RFTk0v@9d>@A&hjbj&2&1Ha$whu`^dXsew*Pki01T96==-rY43 zzjEbVF>|wGxv7JLg9t>Bs+~U|j-#I=nDlsfJQd~czCB-AI2#6E-a&Kn=jW#dytYEf z7Q5L9E=P|%C1erlw=mA0V8q!E@U#GkaEbG1>YW#e4shT3ju-6G#8f`!wKv&se4MC1 zhjzt6wqOqh-gfh$4fVp@D%)_?1JKf_x@T+oJE1r-0Tqf6mAsgD?3tH?#6S+;CsMf6 z0!(*Rz}7^%QJE(Q#(m7$f(;Kgs!#*U$lY9Rrj`#RwDHnq&vMzwfVW*|OCEs4;W3`U z`vO^$io*Zn>83W z8ZpFzX;5utX-3&8VxJRf89bkdfiNsakY`A6(6Ew3SWO zwbfNApni39#6lNlNQO#AmK^aaQJd(Wc7%XPKG>lV-g4oM=VwvNNzvhsYrI!^_@NZA znD`ZkWxHiH78&fk!!Xs+-fnOyqV>sW{su8NP68L7-eNrC;bDQQg~S08#^D83anPd7 zcfar}OYXzj9kT7ho#j!GenbAaQN%}5&u4A9Rwro_otY~H)+$Lp=YvV!13x3|MNTFV zS3JCvsWCOyW_E<5>uJDODcqd~DaexeY9WTvb|SRl%X-JcTAs&?=2X@XpTFoLi-}6v z@%h&?8-TPo=Mj|Ae;5VhuWz(y3l%1iqnOE80>JW@md2;YWG=qamq#%$8duscN@f3Lb^fqyOyg&hmf)q=Jv+0w{G zvKzUrq)&USP<(MuSS@Um`5PzF2pWd0cuy-Q2PvW6gID}g`a?vyY$Nf(>$X^X*n8}{ zf~CsUOXEpWHA>&2?x5ebJk$!eI=0qvr9Q&=52Wv(R4^E3dbEMDdG^C5kRt&z7j*5+8pZjE)kU zCX>t4B@BD8+f3tVQ7|W|mdNJ&Mzj7YsyAjpL0J9^e$+Dy$Ve@VEi<0vOR<8E>vhEb zeE34n1i+V4&-5eNN2X}f<0DB7IDyF9 z{MuUd#Ske3$VH!JfsB|2%)hAUG%q>Cdvx(Tfr8I5d9=K|k~>D+ov*v^T(!Y!rl_*J zAYd$memBMwPrcLIV;4lli}xDbG$4rtYZ(u~hF{u4&rgr*21br%P9r}*_F2teC_D#s zQo!7IG{H`f6I9bNPjUI%8~LC;34ewAK)48<%J}7%t?})Zv>7KYYFC}vC~T3&=F2gh zBjN>|9EhS(4nS8ta~oc|1t%DzR37X~m9Hr7r;3iYG8HU|zvZAKjmLEi`p|VZZR{$I zGPf5@T1IKQ&W>hd8Z1kBr`+lop{Ihm=Cu6Ox%zp&U&OL~jAFN$uQ)oifkKm=LNRFNjzU1r)d*{^Pk!z1e)I7YGjT~rX+o%L)E#gLzlFtP3s ze{aNlaE2;Jk0O&AGoYCfv*qs>+b0J$KE@a=#5J{{5%y`;*+f@lQ zJlrN>e_yeljxf^um7hN^h!&u=Stie52e#c&{Ax zQ69^C^CTl4uci`B)1+l+Abop)?mm&&mZL0Rx5CWp#dF_HGKcT;eQr0Ql6Mjqmde>z zfh9b^U#HHLE>I7W{_$xt4ZG>)5PZ*m!egH8Ev2+xs?Sk~0Z~X@ymvsmLtgN^}&m(8QUL_d5 zYly5hBBiQ@BbO6NOLVOn=U9EAQoX$hwTh6=6)3BX(BoVR&m{y?|(gw zDolst|N8S9DM=oxSu@A-`@KH*_sE|~E$pFsF)%YLYE#-K12rra?S%678^?TaUW}-6 zGo!+8XREgmwk!oY(4W$U*YAoT$6^OxM??p))$DUixxEybFuQ1ACi=b0Yj}u;Ht2km zb#5*%WnCQ|@j$k8xTC25nZ-Xr5lW^O@?@1|3Hf+p{A(b^*{s2XLSA}-FbhQ%>|%CQ z{Dhs}hbCaQdS8pZAauVr)q#OO)lZ|9@Mb$hgFBW7Nb0+kfBl;EPvO#o4p1zA0yT8! z%~dOjGj)3;*x0a#*EKS6BCAHSB7*H8%vUuD0V^GPO6t4~3} zAgYTsMdP)9T;oUUp5_Watmrcf@ax?gbf)3aY+{j#6{;Yv+1o`mlIc-ZG5Q?6qb4R+ zL8x`V*~k-qseT2DkjvG`>QRLV$J&h)LySY%whkhW^-RSVTql!`1bf3aKS##jmmvj})P=9Qv~KoY@xoAv?6!|B2Xp-9_Kk zNbDsh#Emi7Y_z%V^De=#=v6Oig!dX)hY{5?##y~Q-oio9je^ytIJg( z62)%k>-z3tdsr!*A_160xW5`_e_Z!WzLT0;02@<25lU+J@bzW_?zL~77x)qnR&f^m zWjEnn_09y}pZEXwoWTD4@L;v%fNewCMdCvjpNhw|re<=KHcNiiSsJDQs_^rLLaUdp zKWe}qG9KYfQ0tgk9CkPV1*nmCwwLAt#T#tsNH-CQ2AsVD!3yZII!I8Nlx+$QmLQ)E z*wA)U`9+MX=Lnb6GyUl6l*YV;``11=pKU|T6>2;dy{h-X5=FD@K4e^YKsYiKidk0-4+rNYV?txCDbbUu!&z%uuEOF2leXPUv7`Nd{y~P9_tsDWx$|ngl z_x2c+CZ}MlcV1C!=rqP@^a@@Q0IlpmJDPiME5*o-gd)oFT>tsRau< zC9QyKG2~}8()3wbjGe7>%h~Dj{b6yoZy-u76&L59^Gv@x(f?+5S6Mk7J$%5z`xOxc z6v?#8R>=0^0*n>Txsx$xLi)Z?Ioyh|xNW#X<>u{p*M9FJ?KJ!Q8d6IBCzV!`I?H8V zC#Au1Dz6$k+?z{Ayu8!%p{15e^L@JyYL&eCZd=sH2o}+>wNfvx74&lTB?14^!In>j zM=A!>aXwfGF7q1LuN4Rp{C1n$H^Ag-O3Ym-PkD2@=u|t=M6}Zer=Ri99R_y0Mk2|| z$1rSrV9kw)u8|CHP1ao zG#-6f*}W1q>JV?~#f$B6k?)P?#O;9p^(UEWWf(tI{mYUOw0>|I3OqO)E7pa=N2Y>|zrP+rX$AEUrG+MYJ!(+FpUE0gUk(AH>x)RhN#SWUnxk1TJ&_AxqVK zJpAX0{MUHOxHS=f->}hIqx+QGde(AQ998g!mW#DL#xBDIC;jj55V*5vA$YxzFsQOH zQSwR-P9_2w#pkoO^Um}6$`x`&9FSw7(plk6FaB5rgP zwzUVeN%=qlcSdxu%%7_EB zvhkRWTEaP)Yigl(zb3V%N$(?T45PbH2I2NU{nVK?w|6-fiu^Xk)+)9CpQngPYTg*+ z^}E=i5>;*jb?m3uSPJQAN~ECo!Y-HI>D-6u`lMr*1BN;g2Sk49_8oC*5&DuSW1IgV z)<*c)!^zt@B6_N^?TDcNkZdBZP;SlEs8JNAQhu(x(3q6+vnm2OYWbMLvO%SD^&&$;i3r9j zE~yCRsFZYkz7LziZ_hJQ{a5Vz$M`$AMN`FLYvSo~nOSbm&?I}d&-a(LRr+nrIF+ma z$OI2YjNRg=sQ4tC4siAJow|+&x#9DiOW7oYduR1DRS_!iQGyt%+pqZ6xNp-BW@N=f zKde;+EY(5PjiJC&Li74`ThuM95ge{HS<(0H+IK$ep^k00GjHF^9(*7QX_Y7rj@$~L zPG5i6$wtYmDXE!*!-g+#l&ymaxH!Rk_d7Cb63u6=y#N)p3;Tv2WxdM14J+aK-F*|~ z(^n+Ej>t>rx;AM2nr_rZ{(ci%&BzQbVkqg4tWIh94i?)Rg>_+)fS(Wo4d zyr{87kHcBZzQOyc7Ku0Q^U7L)f3L^8{0x%c?#a!=>!mapC)VE*FGhXM%<@hpgJrR>Mi7kj zij5RS$NA6O%Qxn(?>79J7Qlg}CUNsl6){CDd@8x{`5!Buotyv3JruOp4 zBbiuWtz}UJV8<2Fm6G%5lm%66W=VOHvM(c8I`3uMU%r}+B=SzI_hkrqO9%2Mi4)}@ zIjhp{@OPpHWyA}RC8*49#+ktoLc@Wu7x&UI&`SB*{m%5=)?8T1%c7q+CuhEhvgz;_ zj>4i6w}UKpmEk3+dumPk5uqq)1-R{&!5PbDT$|@ST>fAKxK5y}Sts<5cuidJtW%e$ zlSaz$63unT1Kzz3hxM+IFW0h|o_inv1#|9y8yr|)%L1$!v&)^P^f|fO=?v8jpTs-i9Hcg`ZJk`5 z+;L|3FN$=3AAW8r@0j|jx#~GMLFn&r@~uwHSJs&D_b@V6Z*NVR=42XAl92>JVy!ixixBi*>Ej;F44!QQ^6m~XYc^X>DettUrhPGqcX+r)HaUt2q zbeZH$czaNd!izqG--Nb?e0DM69Hs|>jA4cd`()u*uCUIY1K*3xoIYupJeFJ3ger75 zJ|mtWuez{kb{@eVL8+S4lZB^1xM_wMe7K|TP8wpP8N}gLhnP1)Keesn5rg#=9&ZrM zi5>&bzoL0c)0XkZ?KW`JX;`$1ttE8!NTp4ZMT1J9qv-HuIbNv9XVIqLq38}-3e8h& zSBGJw0+(}WF(}neW|@;*1^L}}lA=gF8k?73wnVvJ29<9s>7sQe8|JWYodH(q-A1og zlakaZjbN1UH>1e(#~>_qPV`Z^=HyKS`k0y|E&RHg8`z j+FnLhXTPmsyCS8pY|#c3+N-}`+y-PN6(y>~i~{}-cZF#7 diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml deleted file mode 100644 index 766e8972d9..0000000000 --- a/demos/cast/src/main/res/values/strings.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Exo Cast Demo - - Cast - - DRM scheme not supported by this device. - - diff --git a/extensions/cast/README.md b/extensions/cast/README.md deleted file mode 100644 index 73f7041729..0000000000 --- a/extensions/cast/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# ExoPlayer Cast extension # - -## Description ## - -The cast extension is a [Player][] implementation that controls playback on a -Cast receiver app. - -[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html - -## Getting the extension ## - -The easiest way to use the extension is to add it as a gradle dependency: - -```gradle -compile 'com.google.android.exoplayer:extension-cast:rX.X.X' -``` - -where `rX.X.X` is the version, which must match the version of the ExoPlayer -library being used. - -Alternatively, you can clone the ExoPlayer repository and depend on the module -locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. - -[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md - -## Using the extension ## - -Create a `CastPlayer` and use it to integrate Cast into your app using -ExoPlayer's common Player interface. You can try the Cast Extension to see how a -[PlaybackControlView][] can be used to control playback in a remote receiver app. - -[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle deleted file mode 100644 index 7d252332c9..0000000000 --- a/extensions/cast/build.gradle +++ /dev/null @@ -1,45 +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. -apply from: '../../constants.gradle' -apply plugin: 'com.android.library' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion 14 - targetSdkVersion project.ext.targetSdkVersion - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion - compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion - compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-ui') -} - -ext { - javadocTitle = 'Cast extension' -} -apply from: '../../javadoc_library.gradle' - -ext { - releaseArtifact = 'extension-cast' - releaseDescription = 'Cast extension for ExoPlayer.' -} -apply from: '../../publish.gradle' diff --git a/extensions/cast/src/main/AndroidManifest.xml b/extensions/cast/src/main/AndroidManifest.xml deleted file mode 100644 index c12fc1289f..0000000000 --- a/extensions/cast/src/main/AndroidManifest.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - 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 deleted file mode 100644 index ffb06ed232..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ /dev/null @@ -1,853 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.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.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.MediaQueueItem; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.MediaTrack; -import com.google.android.gms.cast.framework.CastContext; -import com.google.android.gms.cast.framework.CastSession; -import com.google.android.gms.cast.framework.SessionManager; -import com.google.android.gms.cast.framework.SessionManagerListener; -import com.google.android.gms.cast.framework.media.RemoteMediaClient; -import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; -import com.google.android.gms.common.api.PendingResult; -import com.google.android.gms.common.api.ResultCallback; -import java.util.List; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * {@link Player} implementation that communicates with a Cast receiver app. - * - *

      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.

      - * - *

      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.

      - * - *

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

      - */ -public final class CastPlayer implements Player { - - /** - * Listener of changes in the cast session availability. - */ - public interface SessionAvailabilityListener { - - /** - * Called when a cast session becomes available to the player. - */ - void onCastSessionAvailable(); - - /** - * Called when the cast session becomes unavailable. - */ - void onCastSessionUnavailable(); - - } - - private static final String TAG = "CastPlayer"; - - private static final int RENDERER_COUNT = 3; - private static final int RENDERER_INDEX_VIDEO = 0; - private static final int RENDERER_INDEX_AUDIO = 1; - private static final int RENDERER_INDEX_TEXT = 2; - private static final long PROGRESS_REPORT_PERIOD_MS = 1000; - private static final TrackGroupArray EMPTY_TRACK_GROUP_ARRAY = new TrackGroupArray(); - private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY = - new TrackSelectionArray(null, null, null); - private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; - - 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 SeekResultCallback seekResultCallback; - - // Listeners. - private final CopyOnWriteArraySet listeners; - private SessionAvailabilityListener sessionAvailabilityListener; - - // Internal state. - 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 pendingSeekCount; - private int pendingSeekWindowIndex; - private long pendingSeekPositionMs; - - /** - * @param castContext The context from which the cast session is obtained. - */ - public CastPlayer(CastContext castContext) { - this.castContext = castContext; - window = new Timeline.Window(); - period = new Timeline.Period(); - statusListener = new StatusListener(); - 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 a single item media queue. If no session is available, does nothing. - * - * @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 PendingResult loadItem(MediaQueueItem item, long positionMs) { - return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); - } - - /** - * 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; - } - - /** - * Sets a listener for updates on the cast session availability. - * - * @param listener The {@link SessionAvailabilityListener}. - */ - public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { - sessionAvailabilityListener = listener; - } - - // Player implementation. - - @Override - public void addListener(EventListener listener) { - listeners.add(listener); - } - - @Override - public void removeListener(EventListener listener) { - listeners.remove(listener); - } - - @Override - public int getPlaybackState() { - return playbackState; - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - if (remoteMediaClient == null) { - return; - } - if (playWhenReady) { - remoteMediaClient.play(); - } else { - remoteMediaClient.pause(); - } - } - - @Override - public boolean getPlayWhenReady() { - return playWhenReady; - } - - @Override - public void seekToDefaultPosition() { - seekTo(0); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - seekTo(windowIndex, 0); - } - - @Override - public void seekTo(long positionMs) { - seekTo(getCurrentWindowIndex(), positionMs); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - 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); - } - pendingSeekCount++; - pendingSeekWindowIndex = windowIndex; - pendingSeekPositionMs = positionMs; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } - } else if (pendingSeekCount == 0) { - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } - } - } - - @Override - public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - // Unsupported by the RemoteMediaClient API. Do nothing. - } - - @Override - public PlaybackParameters getPlaybackParameters() { - return PlaybackParameters.DEFAULT; - } - - @Override - public void stop() { - if (remoteMediaClient != null) { - remoteMediaClient.stop(); - } - } - - @Override - public void release() { - castContext.getSessionManager().removeSessionManagerListener(statusListener, CastSession.class); - } - - @Override - public int getRendererCount() { - // We assume there are three renderers: video, audio, and text. - return RENDERER_COUNT; - } - - @Override - public int getRendererType(int index) { - switch (index) { - case RENDERER_INDEX_VIDEO: - return C.TRACK_TYPE_VIDEO; - case RENDERER_INDEX_AUDIO: - return C.TRACK_TYPE_AUDIO; - case RENDERER_INDEX_TEXT: - return C.TRACK_TYPE_TEXT; - default: - throw new IndexOutOfBoundsException(); - } - } - - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - if (remoteMediaClient != null) { - remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null); - } - } - - @Override - @RepeatMode public int getRepeatMode() { - return repeatMode; - } - - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - // TODO: Support shuffle mode. - } - - @Override - public boolean getShuffleModeEnabled() { - // TODO: Support shuffle mode. - return false; - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return currentTrackSelection; - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - return currentTrackGroups; - } - - @Override - public Timeline getCurrentTimeline() { - return currentTimeline; - } - - @Override - @Nullable public Object getCurrentManifest() { - return null; - } - - @Override - public int getCurrentPeriodIndex() { - return getCurrentWindowIndex(); - } - - @Override - public int getCurrentWindowIndex() { - return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex; - } - - @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 - : currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); - } - - @Override - public long getCurrentPosition() { - return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs - : remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition() - : lastReportedPositionMs; - } - - @Override - public long getBufferedPosition() { - return getCurrentPosition(); - } - - @Override - public int getBufferedPercentage() { - long position = getBufferedPosition(); - long duration = getDuration(); - return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0 - : duration == 0 ? 100 - : Util.constrainValue((int) ((position * 100) / duration), 0, 100); - } - - @Override - public boolean isCurrentWindowDynamic() { - return !currentTimeline.isEmpty() - && currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic; - } - - @Override - public boolean isCurrentWindowSeekable() { - return !currentTimeline.isEmpty() - && currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable; - } - - @Override - public boolean isPlayingAd() { - return false; - } - - @Override - public int getCurrentAdGroupIndex() { - return C.INDEX_UNSET; - } - - @Override - public int getCurrentAdIndexInAdGroup() { - return C.INDEX_UNSET; - } - - @Override - public boolean isLoading() { - return false; - } - - @Override - public long getContentPosition() { - return getCurrentPosition(); - } - - // 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 && pendingSeekCount == 0) { - this.currentWindowIndex = currentWindowIndex; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); - } - } - 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. - return; - } - if (this.remoteMediaClient != null) { - this.remoteMediaClient.removeListener(statusListener); - this.remoteMediaClient.removeProgressListener(statusListener); - } - this.remoteMediaClient = remoteMediaClient; - if (remoteMediaClient != null) { - if (sessionAvailabilityListener != null) { - sessionAvailabilityListener.onCastSessionAvailable(); - } - remoteMediaClient.addListener(statusListener); - remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); - updateInternalState(); - } else { - if (sessionAvailabilityListener != null) { - sessionAvailabilityListener.onCastSessionUnavailable(); - } - } - } - - private @Nullable MediaStatus getMediaStatus() { - return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : 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; - } - } - - /** - * 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; - } - 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(); - } - } - - /** - * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If - * there is no media session, returns 0. - */ - private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) { - Integer currentItemId = mediaStatus != null - ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null; - return currentItemId != null ? currentItemId : 0; - } - - private static boolean isTrackActive(long id, long[] activeTrackIds) { - for (long activeTrackId : activeTrackIds) { - if (activeTrackId == id) { - return true; - } - } - return false; - } - - private static int getRendererIndexForTrackType(int trackType) { - return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO - : trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO - : trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT - : 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 { - - // RemoteMediaClient.ProgressListener implementation. - - @Override - public void onProgressUpdated(long progressMs, long unusedDurationMs) { - lastReportedPositionMs = progressMs; - } - - // RemoteMediaClient.Listener implementation. - - @Override - public void onStatusUpdated() { - updateInternalState(); - } - - @Override - public void onMetadataUpdated() {} - - @Override - public void onQueueStatusUpdated() { - maybeUpdateTimelineAndNotify(); - } - - @Override - public void onPreloadStatusUpdated() {} - - @Override - public void onSendingRemoteMediaRequest() {} - - @Override - public void onAdBreakStatusUpdated() {} - - - // SessionManagerListener implementation. - - @Override - public void onSessionStarted(CastSession castSession, String s) { - setRemoteMediaClient(castSession.getRemoteMediaClient()); - } - - @Override - public void onSessionResumed(CastSession castSession, boolean b) { - setRemoteMediaClient(castSession.getRemoteMediaClient()); - } - - @Override - public void onSessionEnded(CastSession castSession, int i) { - setRemoteMediaClient(null); - } - - @Override - public void onSessionSuspended(CastSession castSession, int i) { - setRemoteMediaClient(null); - } - - @Override - public void onSessionResumeFailed(CastSession castSession, int statusCode) { - Log.e(TAG, "Session resume failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); - } - - @Override - public void onSessionStarting(CastSession castSession) { - // Do nothing. - } - - @Override - public void onSessionStartFailed(CastSession castSession, int statusCode) { - Log.e(TAG, "Session start failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); - } - - @Override - public void onSessionEnding(CastSession castSession) { - // Do nothing. - } - - @Override - public void onSessionResuming(CastSession castSession, String s) { - // Do nothing. - } - - } - - // Result callbacks hooks. - - private final class SeekResultCallback implements ResultCallback { - - @Override - public void onResult(@NonNull MediaChannelResult result) { - int statusCode = result.getStatus().getStatusCode(); - if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) { - Log.e(TAG, "Seek failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); - } - if (--pendingSeekCount == 0) { - pendingSeekWindowIndex = C.INDEX_UNSET; - pendingSeekPositionMs = C.TIME_UNSET; - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } - } - } - } - -} 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 deleted file mode 100644 index 39b57148b2..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.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/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java deleted file mode 100644 index de60437444..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.cast; - -import com.google.android.exoplayer2.Format; -import com.google.android.gms.cast.CastStatusCodes; -import com.google.android.gms.cast.MediaTrack; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Utility methods for ExoPlayer/Cast integration. - */ -/* package */ final class CastUtils { - - private static final Map CAST_STATUS_CODE_TO_STRING; - - /** - * Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of - * {@link CastStatusCodes}. - * - * @param statusCode A Cast API status code. - * @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of - * {@link CastStatusCodes}. - */ - public static String getLogString(int statusCode) { - String description = CAST_STATUS_CODE_TO_STRING.get(statusCode); - return description != null ? description : "Unknown."; - } - - /** - * Creates a {@link Format} instance containing all information contained in the given - * {@link MediaTrack} object. - * - * @param mediaTrack The {@link MediaTrack}. - * @return The equivalent {@link Format}. - */ - public static Format mediaTrackToFormat(MediaTrack mediaTrack) { - return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(), - null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage()); - } - - static { - HashMap statusCodeToString = new HashMap<>(); - statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_FOUND, - "A requested application could not be found."); - statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_RUNNING, - "A requested application is not currently running."); - statusCodeToString.put(CastStatusCodes.AUTHENTICATION_FAILED, "Authentication failure."); - statusCodeToString.put(CastStatusCodes.CANCELED, "An in-progress request has been " - + "canceled, most likely because another action has preempted it."); - statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_CREATION_FAILED, - "The Cast Remote Display service could not be created."); - statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_DISCONNECTED, - "The Cast Remote Display service was disconnected."); - statusCodeToString.put(CastStatusCodes.FAILED, "The in-progress request failed."); - statusCodeToString.put(CastStatusCodes.INTERNAL_ERROR, "An internal error has occurred."); - statusCodeToString.put(CastStatusCodes.INTERRUPTED, - "A blocking call was interrupted while waiting and did not run to completion."); - statusCodeToString.put(CastStatusCodes.INVALID_REQUEST, "An invalid request was made."); - statusCodeToString.put(CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL, "A message could " - + "not be sent because there is not enough room in the send buffer at this time."); - statusCodeToString.put(CastStatusCodes.MESSAGE_TOO_LARGE, - "A message could not be sent because it is too large."); - statusCodeToString.put(CastStatusCodes.NETWORK_ERROR, "Network I/O error."); - statusCodeToString.put(CastStatusCodes.NOT_ALLOWED, - "The request was disallowed and could not be completed."); - statusCodeToString.put(CastStatusCodes.REPLACED, - "The request's progress is no longer being tracked because another request of the same type" - + " has been made before the first request completed."); - statusCodeToString.put(CastStatusCodes.SUCCESS, "Success."); - statusCodeToString.put(CastStatusCodes.TIMEOUT, "An operation has timed out."); - statusCodeToString.put(CastStatusCodes.UNKNOWN_ERROR, - "An unknown, unexpected error has occurred."); - CAST_STATUS_CODE_TO_STRING = Collections.unmodifiableMap(statusCodeToString); - } - - private CastUtils() {} - -} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java deleted file mode 100644 index 06f0bec971..0000000000 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.cast; - -import android.content.Context; -import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.framework.CastOptions; -import com.google.android.gms.cast.framework.OptionsProvider; -import com.google.android.gms.cast.framework.SessionProvider; -import java.util.List; - -/** - * A convenience {@link OptionsProvider} to target the default cast receiver app. - */ -public final class DefaultCastOptionsProvider implements OptionsProvider { - - @Override - public CastOptions getCastOptions(Context context) { - return new CastOptions.Builder() - .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) - .setStopReceiverApplicationWhenEndingSession(true).build(); - } - - @Override - public List getAdditionalSessionProviders(Context context) { - return null; - } - -} diff --git a/settings.gradle b/settings.gradle index d4530d67b7..0a404aad73 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,11 +19,9 @@ if (gradle.ext.has('exoplayerModulePrefix')) { } include modulePrefix + 'demo' -include modulePrefix + 'demo-cast' include modulePrefix + 'demo-ima' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') -project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') project(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') From a361f9043c9c758da6089871e67a007817765b8f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Nov 2017 09:31:52 -0800 Subject: [PATCH 028/148] Remove stray TV directory from 2.6.0 release --- demos/tv/README.md | 4 ---- demos/tv/build.gradle | 49 ------------------------------------------- 2 files changed, 53 deletions(-) delete mode 100644 demos/tv/README.md delete mode 100644 demos/tv/build.gradle diff --git a/demos/tv/README.md b/demos/tv/README.md deleted file mode 100644 index 8a1ab807a0..0000000000 --- a/demos/tv/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# TV tuner demo application # - -This folder contains a demo application that uses ExoPlayer to play broadcast -TV from USB tuners. diff --git a/demos/tv/build.gradle b/demos/tv/build.gradle deleted file mode 100644 index 9e87d5e06b..0000000000 --- a/demos/tv/build.gradle +++ /dev/null @@ -1,49 +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. -apply from: '../../constants.gradle' -apply plugin: 'com.android.application' - -android { - compileSdkVersion project.ext.compileSdkVersion - buildToolsVersion project.ext.buildToolsVersion - - defaultConfig { - minSdkVersion 21 - targetSdkVersion project.ext.targetSdkVersion - } - - buildTypes { - release { - shrinkResources true - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt') - } - debug { - jniDebuggable = true - } - } - - lintOptions { - // The demo app does not have translations. - disable 'MissingTranslation' - } - -} - -dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'internal-extension-tv') - compile project(modulePrefix + 'extension-ffmpeg') -} From be0648541fe4d7cd2c8b44d57822a379c381a7db Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 03:01:58 -0800 Subject: [PATCH 029/148] Forward ad group and ad index when creating period from concatanted media sources. Also added tests which verify the intended behaviour. GitHub:#3452 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175656478 --- .../source/ConcatenatingMediaSourceTest.java | 26 ++++++++ .../DynamicConcatenatingMediaSourceTest.java | 63 +++++++++++-------- .../source/ConcatenatingMediaSource.java | 4 +- .../DynamicConcatenatingMediaSource.java | 3 +- .../exoplayer2/source/LoopingMediaSource.java | 3 +- .../exoplayer2/testutil/FakeMediaSource.java | 21 +++++-- .../exoplayer2/testutil/FakeTimeline.java | 35 ++++++++++- .../exoplayer2/testutil/TimelineAsserts.java | 61 +++++++++++++++++- 8 files changed, 180 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 53111e83ac..6f6556225e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -196,6 +197,31 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } + public void testPeriodCreationWithAds() throws InterruptedException { + // Create media source with ad child source. + Timeline timelineContentOnly = new FakeTimeline( + new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); + Timeline timelineWithAds = new FakeTimeline( + new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); + FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); + FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, + mediaSourceWithAds); + + // Prepare and assert timeline contains ad groups. + Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + + // Create all periods and assert period creation of child media sources has been called. + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } + /** * 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 c0c5252751..e506d0a4b3 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 @@ -154,7 +154,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(0, timeline.getLastWindowIndex(true)); // Assert all periods can be prepared. - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); // Remove at front of queue. mediaSource.removeMediaSource(0); @@ -205,7 +206,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); @@ -239,7 +241,8 @@ 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()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); //Add lazy sources after preparation (and also try to prepare media period from lazy source). mediaSource.addMediaSource(1, lazySources[2]); @@ -335,7 +338,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(2, timeline.getLastWindowIndex(false)); assertEquals(2, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); - assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); } public void testIllegalArguments() { @@ -533,6 +537,35 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { waitForCustomRunnable(); } + public void testPeriodCreationWithAds() throws InterruptedException { + // Create dynamic media source with ad child source. + Timeline timelineContentOnly = new FakeTimeline( + new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); + Timeline timelineWithAds = new FakeTimeline( + new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); + FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); + FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + mediaSource.addMediaSource(mediaSourceContentOnly); + mediaSource.addMediaSource(mediaSourceWithAds); + assertNull(timeline); + + // Prepare and assert timeline contains ad groups. + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + + // Create all periods and assert period creation of child media sources has been called. + TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, + TIMEOUT_MS); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } + private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() throws InterruptedException { HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); @@ -616,28 +649,6 @@ 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 DynamicConcatenatingMediaSourceAndHandler { public final DynamicConcatenatingMediaSource mediaSource; 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 fe8f23c4c0..058471f31f 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 @@ -120,8 +120,8 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); - MediaPeriodId periodIdInSource = - new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); + MediaPeriodId periodIdInSource = id.copyWithPeriodIndex( + id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 6bfa4047a5..e80abad3ef 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 @@ -341,7 +341,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); - MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild); + MediaPeriodId idInSource = id.copyWithPeriodIndex( + id.periodIndex - holder.firstPeriodIndexInChild); MediaPeriod mediaPeriod; if (!holder.isPrepared) { mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); 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 583c1ed68c..984820cc6a 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 @@ -80,7 +80,8 @@ public final class LoopingMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return loopCount != Integer.MAX_VALUE - ? childSource.createPeriod(new MediaPeriodId(id.periodIndex % childPeriodCount), allocator) + ? childSource.createPeriod(id.copyWithPeriodIndex(id.periodIndex % childPeriodCount), + allocator) : childSource.createPeriod(id, allocator); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index cef48285ca..1f2524110a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -38,6 +38,7 @@ public class FakeMediaSource implements MediaSource { private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; + private final ArrayList createdMediaPeriods; private boolean preparedSource; private boolean releasedSource; @@ -58,13 +59,10 @@ public class FakeMediaSource implements MediaSource { this.timeline = timeline; this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); + this.createdMediaPeriods = new ArrayList<>(); this.trackGroupArray = trackGroupArray; } - public void assertReleased() { - Assert.assertTrue(releasedSource); - } - @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assert.assertFalse(preparedSource); @@ -84,6 +82,7 @@ public class FakeMediaSource implements MediaSource { Assert.assertFalse(releasedSource); FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); + createdMediaPeriods.add(id); return mediaPeriod; } @@ -104,6 +103,20 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + /** + * Assert that the source and all periods have been released. + */ + public void assertReleased() { + Assert.assertTrue(releasedSource); + } + + /** + * Assert that a media period for the given id has been created. + */ + public void assertMediaPeriodCreated(MediaPeriodId mediaPeriodId) { + Assert.assertTrue(createdMediaPeriods.contains(mediaPeriodId)); + } + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { return new FakeMediaPeriod(trackGroupArray); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 040782264b..2937ee2770 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; /** * Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. @@ -36,6 +37,8 @@ public final class FakeTimeline extends Timeline { public final boolean isSeekable; public final boolean isDynamic; public final long durationUs; + public final int adGroupsPerPeriodCount; + public final int adsPerAdGroupCount; public TimelineWindowDefinition(int periodCount, Object id) { this(periodCount, id, true, false, WINDOW_DURATION_US); @@ -47,15 +50,24 @@ public final class FakeTimeline extends Timeline { public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) { + this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); + } + + public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, + boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { this.periodCount = periodCount; this.id = id; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.durationUs = durationUs; + this.adGroupsPerPeriodCount = adGroupsCountPerPeriod; + this.adsPerAdGroupCount = adsPerAdGroupCount; } } + private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; + private final TimelineWindowDefinition[] windowDefinitions; private final int[] periodOffsets; @@ -96,7 +108,28 @@ public final class FakeTimeline extends Timeline { Object id = setIds ? windowPeriodIndex : null; Object uid = setIds ? periodIndex : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; - return period.set(id, uid, windowIndex, periodDurationUs, periodDurationUs * windowPeriodIndex); + long positionInWindowUs = periodDurationUs * windowPeriodIndex; + if (windowDefinition.adGroupsPerPeriodCount == 0) { + return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs); + } else { + int adGroups = windowDefinition.adGroupsPerPeriodCount; + long[] adGroupTimesUs = new long[adGroups]; + int[] adCounts = new int[adGroups]; + int[] adLoadedAndPlayedCounts = new int[adGroups]; + long[][] adDurationsUs = new long[adGroups][]; + long adResumePositionUs = 0; + long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0; + for (int i = 0; i < adGroups; i++) { + adGroupTimesUs[i] = i * adGroupOffset; + adCounts[i] = windowDefinition.adsPerAdGroupCount; + adLoadedAndPlayedCounts[i] = 0; + adDurationsUs[i] = new long[adCounts[i]]; + Arrays.fill(adDurationsUs[i], AD_DURATION_US); + } + return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs, adGroupTimesUs, + adCounts, adLoadedAndPlayedCounts, adLoadedAndPlayedCounts, adDurationsUs, + adResumePositionUs); + } } @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 c61aac708c..b1df8f62e1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -16,12 +16,19 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; /** * Unit test for {@link Timeline}. @@ -60,7 +67,7 @@ public final class TimelineAsserts { } /** - * Asserts that window properties {@link Window}.isDynamic are set correctly.. + * Asserts that window properties {@link Window}.isDynamic are set correctly. */ public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { Window window = new Window(); @@ -139,5 +146,57 @@ public final class TimelineAsserts { } } + /** + * Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. + */ + public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) { + Period period = new Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + timeline.getPeriod(i, period); + assertEquals(expectedAdGroupCounts[i], period.getAdGroupCount()); + } + } + + /** + * Asserts that all period (including ad periods) can be created from the source, prepared, and + * released without exception and within timeout. + */ + public static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + Timeline timeline, long timeoutMs) { + Period period = new Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, new MediaPeriodId(i), timeoutMs); + timeline.getPeriod(i, period); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, + new MediaPeriodId(i, adGroupIndex, adIndex), timeoutMs); + } + } + } + } + + private static void assertPeriodCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + MediaPeriodId mediaPeriodId, long timeoutMs) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(mediaPeriodId, 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) {} + }, /* positionUs= */ 0); + assertTrue(mediaPeriodPrepared.block(timeoutMs)); + // MediaSource is supposed to support multiple calls to createPeriod with the same id without an + // intervening call to releasePeriod. + MediaPeriod secondMediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); + assertNotNull(secondMediaPeriod); + mediaSource.releasePeriod(secondMediaPeriod); + mediaSource.releasePeriod(mediaPeriod); + } + } From 0a8d2423851a8b96f6d4c19005b01aec21d5bd2b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 17 Nov 2017 19:34:27 +0000 Subject: [PATCH 030/148] Amend release notes for 2.6.0 release --- RELEASENOTES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d6b20be4e1..b645cc81a0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -60,7 +60,6 @@ ([#3406](https://github.com/google/ExoPlayer/issues/3406)). * New Leanback extension: Simplifies binding Exoplayer to Leanback UI components. -* New Cast extension: Simplifies toggling between local and Cast playbacks. * Unit tests moved to Robolectric. * Misc bugfixes. From 8940e7b32a188be36f68e244dddbe66456884e66 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 17 Nov 2017 06:33:32 -0800 Subject: [PATCH 031/148] Replace hard coded UUID in OfflineLicenseHelper with a parameter ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176102179 --- .../exoplayer2/drm/OfflineLicenseHelperTest.java | 5 ++--- .../exoplayer2/drm/OfflineLicenseHelper.java | 15 +++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) 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 35bfbe613a..22ae57932b 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,7 +23,6 @@ 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.upstream.HttpDataSource; import java.util.HashMap; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -34,7 +33,6 @@ import org.mockito.MockitoAnnotations; public class OfflineLicenseHelperTest extends InstrumentationTestCase { private OfflineLicenseHelper offlineLicenseHelper; - @Mock private HttpDataSource httpDataSource; @Mock private MediaDrmCallback mediaDrmCallback; @Mock private ExoMediaDrm mediaDrm; @@ -42,7 +40,8 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { protected void setUp() throws Exception { setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); - offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null); + offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, + null); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index cafe41ed09..481bea66c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -27,8 +27,8 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; import java.util.HashMap; +import java.util.UUID; /** * Helper class to download, renew and release offline licenses. @@ -96,7 +96,8 @@ public final class OfflineLicenseHelper { String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + return new OfflineLicenseHelper<>(C.WIDEVINE_UUID, + FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } @@ -104,6 +105,7 @@ public final class OfflineLicenseHelper { /** * Constructs an instance. Call {@link #release()} when the instance is no longer required. * + * @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 @@ -111,7 +113,7 @@ public final class OfflineLicenseHelper { * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, * MediaDrmCallback, HashMap, Handler, EventListener) */ - public OfflineLicenseHelper(ExoMediaDrm mediaDrm, MediaDrmCallback callback, + public OfflineLicenseHelper(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); @@ -137,7 +139,7 @@ public final class OfflineLicenseHelper { conditionVariable.open(); } }; - drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback, + drmSessionManager = new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); } @@ -174,12 +176,9 @@ public final class OfflineLicenseHelper { * * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded. * @return The key set id for the downloaded license. - * @throws IOException If an error occurs reading data from the stream. - * @throws InterruptedException If the thread has been interrupted. * @throws DrmSessionException Thrown when a DRM session error occurs. */ - public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws IOException, - InterruptedException, DrmSessionException { + public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws DrmSessionException { Assertions.checkArgument(drmInitData != null); return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); } From e469269f3c45e6c11737512447ccea6d95f505be Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 03:36:50 -0800 Subject: [PATCH 032/148] Fix some lint issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176337058 --- extensions/ima/build.gradle | 9 +++---- .../exoplayer2/offline/SegmentDownloader.java | 2 +- .../google/android/exoplayer2/util/Util.java | 3 +++ .../dash/manifest/DashManifestParser.java | 2 +- .../source/dash/offline/DashDownloader.java | 4 ++-- .../smoothstreaming/offline/SsDownloader.java | 4 ++-- library/ui/src/main/res/values-v11/styles.xml | 24 ------------------- library/ui/src/main/res/values/styles.xml | 2 +- .../testutil/FakeSimpleExoPlayer.java | 8 ++++++- 9 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 library/ui/src/main/res/values-v11/styles.xml diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 90c0a911d9..5038aaf5b9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -28,10 +28,11 @@ android { dependencies { compile project(modulePrefix + 'library-core') // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: - // com.google.android.gms:play-services-ads:11.2.0 - // |-- com.google.android.gms:play-services-ads-lite:11.2.0 - // |-- com.google.android.gms:play-services-basement:11.2.0 + // com.android.support:support-v4 to be used. Else an older version (25.2.0) + // is included via: + // com.google.android.gms:play-services-ads:11.4.2 + // |-- com.google.android.gms:play-services-ads-lite:11.4.2 + // |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' 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 d81df90b81..3cb5db30ec 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 @@ -265,7 +265,7 @@ public abstract class SegmentDownloader implements Downloader { /** * Returns a list of all segments. * - * @see #getSegments(DataSource, M, Object[], boolean)}. + * @see #getSegments(DataSource, M, Object[], boolean) */ protected abstract List getAllSegments(DataSource dataSource, M manifest, boolean allowPartialIndex) throws InterruptedException, IOException; 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 f47caa046a..a79ed38755 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 @@ -32,6 +32,7 @@ import android.view.Display; import android.view.WindowManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; import java.io.ByteArrayOutputStream; @@ -801,6 +802,8 @@ public final class Util { return channelCount * 3; case C.ENCODING_PCM_32BIT: return channelCount * 4; + case C.ENCODING_INVALID: + case Format.NO_VALUE: default: throw new IllegalArgumentException(); } 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 72df69f7e9..7ffb429784 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 @@ -352,7 +352,7 @@ public class DashManifestParser extends DefaultHandler String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); if (schemeIdUri != null) { - switch (schemeIdUri.toLowerCase()) { + switch (Util.toLowerInvariant(schemeIdUri)) { case "urn:mpeg:dash:mp4protection:2011": schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 558adca7bd..4c07e4874e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -39,8 +39,8 @@ import java.util.List; /** * Helper class to download DASH streams. * - *

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

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

      Example usage: * 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 21cacdc6f3..5e9ae9a164 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 @@ -33,8 +33,8 @@ import java.util.List; /** * Helper class to download SmoothStreaming streams. * - *

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

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

      Example usage: * diff --git a/library/ui/src/main/res/values-v11/styles.xml b/library/ui/src/main/res/values-v11/styles.xml deleted file mode 100644 index 6f77440287..0000000000 --- a/library/ui/src/main/res/values-v11/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 4ef8971ccd..b57cbeaddf 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -16,7 +16,7 @@ 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 01f984b212..4d53a6c89d 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 @@ -215,7 +215,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @SuppressWarnings("ThreadJoinLoop") public void release() { stop(); - playbackThread.quitSafely(); + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + playbackThread.quit(); + } + }); while (playbackThread.isAlive()) { try { playbackThread.join(); From 494237548ada77c3dc56e98fc25dd2c479c7f7d5 Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Tue, 21 Nov 2017 10:59:04 +0000 Subject: [PATCH 033/148] Fix initializationData check for SSA subtitles --- .../java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index d2f5a67c27..12aa1e97d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -59,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null) { + if (initializationData != null && initializationData.size() > 0) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From 1439b4a3ef5a31f15553db30071177c115ea12f5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 21 Nov 2017 13:42:12 +0000 Subject: [PATCH 034/148] Mini cleanup --- .../google/android/exoplayer2/text/ssa/SsaDecoder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 12aa1e97d5..eec4a1269c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -53,13 +53,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } /** - * @param initializationData Optional initialization data for the decoder. If not null, the - * initialization data must consist of two byte arrays. The first must contain an SSA format - * line. The second must contain an SSA header that will be assumed common to all samples. + * @param initializationData Optional initialization data for the decoder. If not null or empty, + * the initialization data must consist of two byte arrays. The first must contain an SSA + * format line. The second must contain an SSA header that will be assumed common to all + * samples. */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null && initializationData.size() > 0) { + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From 60555e2c4b6d74f1276ae30b6524174ac8ee2365 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 08:42:34 -0800 Subject: [PATCH 035/148] Improve robustness of ImaAdsLoader Remove an assertion that there was a call to pause content between two content -> ad transitions. Also, only use the player position for resuming an ad on reattaching if the player is currently playing an ad, in case IMA pauses content before the player actually transitions to an ad. Issue: #3430 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176365842 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b645cc81a0..13218f073f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -53,6 +53,9 @@ ([#3303](https://github.com/google/ExoPlayer/issues/3303)). * Ignore seeks if an ad is playing ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused between + content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). * UI: * Allow specifying a `Drawable` for the `TimeBar` scrubber ([#3337](https://github.com/google/ExoPlayer/issues/3337)). 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 0b11a97f84..5b61db0264 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 @@ -260,7 +260,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void detachPlayer() { if (adsManager != null && imaPausedContent) { - adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); + adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adsManager.pause(); } lastAdProgress = getAdProgress(); @@ -628,7 +628,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { From d51944f4b3fe9586c542e4aec4d3d5c7d81d83a2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 03:44:06 -0800 Subject: [PATCH 036/148] Remove unnecessary dependency ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176487991 --- extensions/mediasession/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 85a8ac46e2..651bd952f8 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -27,7 +27,6 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-media-compat:' + supportLibraryVersion - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion } ext { From b688a562508e74721841aaaa770e9dc47bd378dd Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 04:46:52 -0800 Subject: [PATCH 037/148] Make ExtractorMediaSource timeline dynamic until duration is set We (eventually - albeit possibly infinitely far in the future) expect a timeline update with a window of known duration. This also stops live radio stream playbacks transitioning to ended state when their tracks are disabled. As part of this fix, I found an issue where getPeriodPosition could return null even when defaultPositionProjectionUs is 0, which is not as documented. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176492024 --- .../source/ClippingMediaSourceTest.java | 10 +-- .../source/ExtractorMediaSource.java | 7 +- .../source/SinglePeriodTimeline.java | 23 +++--- .../source/SingleSampleMediaSource.java | 2 +- .../source/SinglePeriodTimelineTest.java | 77 +++++++++++++++++++ 5 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java 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 5e615dbc7f..0fefff86a2 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 @@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 1b3f6cb95c..17f2fc2dad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -204,8 +204,11 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - sourceListener.onSourceInfoRefreshed( - this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); + // If the duration is currently unset, we expect to be able to update the window when its + // duration eventually becomes known. + boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + sourceListener.onSourceInfoRefreshed(this, + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 6f35438444..9cce67f68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline of one period of known duration, and a static window starting at zero and - * extending to that duration. + * Creates a timeline containing a single period and a window that spans it. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable) { - this(durationUs, durationUs, 0, 0, isSeekable, false); + public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { + this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); } /** @@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,11 +106,16 @@ public final class SinglePeriodTimeline extends Timeline { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic) { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the live window. + if (isDynamic && defaultPositionProjectionUs != 0) { + if (windowDurationUs == C.TIME_UNSET) { + // Don't allow projection into a window that has an unknown duration. windowDefaultStartPositionUs = C.TIME_UNSET; + } else { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index dd901958fd..ede2c7ccf9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -110,7 +110,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true); + timeline = new SinglePeriodTimeline(durationUs, true, false); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java new file mode 100644 index 0000000000..94ca8b03f0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import static com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +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 SinglePeriodTimeline}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SinglePeriodTimelineTest { + + private Window window; + private Period period; + + @Before + public void setUp() throws Exception { + window = new Window(); + period = new Period(); + } + + @Test + public void testGetPeriodPositionDynamicWindowUnknownDuration() { + SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + // Should return null with any positive position projection. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); + assertThat(position).isNull(); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + + @Test + public void testGetPeriodPositionDynamicWindowKnownDuration() { + long windowDurationUs = 1000; + SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, + 0, false, true); + // Should return null with a positive position projection beyond window duration. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, + windowDurationUs + 1); + assertThat(position).isNull(); + // Should return (0, duration) with a projection equal to window duration. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(windowDurationUs); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + +} From fa3052d36b78455c80ce7b23e6526edc092b0561 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:56:23 -0800 Subject: [PATCH 038/148] Report additional position discontinuities - Properly report internal discontinuities - Add DISCONTINUITY_REASON_SEEK_ADJUSTMENT to distinguish seek adjustments from other internal discontinuity events ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176367365 --- RELEASENOTES.md | 4 ++++ .../android/exoplayer2/demo/EventLogger.java | 2 ++ .../google/android/exoplayer2/ExoPlayerImpl.java | 15 ++++++++------- .../android/exoplayer2/ExoPlayerImplInternal.java | 12 ++++++++++-- .../com/google/android/exoplayer2/Player.java | 9 +++++++-- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 13218f073f..579c2a92ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,10 @@ * SimpleExoPlayer: Support for multiple video, text and metadata outputs. * Support for `Renderer`s that don't consume any media ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` ([#3362](https://github.com/google/ExoPlayer/issues/3362)). * Fix playbacks involving looping, concatenation and ads getting stuck when diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 68f7ddfd21..27a5c68e28 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -496,6 +496,8 @@ import java.util.Locale; return "PERIOD_TRANSITION"; case Player.DISCONTINUITY_REASON_SEEK: return "SEEK"; + case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return "SEEK_ADJUSTMENT"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: 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 8ee8af5980..349751eb59 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 @@ -473,7 +473,8 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false); + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { @@ -491,11 +492,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted); + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT); break; } case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true); + @DiscontinuityReason int discontinuityReason = msg.arg1; + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { @@ -521,7 +524,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, - boolean positionDiscontinuity) { + boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareAcks -= prepareAcks; pendingSeekAcks -= seekAcks; @@ -542,9 +545,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } if (positionDiscontinuity) { for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity( - seekAcks > 0 ? DISCONTINUITY_REASON_INTERNAL : DISCONTINUITY_REASON_PERIOD_TRANSITION - ); + listener.onPositionDiscontinuity(positionDiscontinuityReason); } } } 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 f00a5ce02d..56e16343ed 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 @@ -503,6 +503,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); } else { // Use the standalone clock if there's no renderer clock, or if the providing renderer has // ended or needs the next sample stream to reenter the ready state. The latter case uses the @@ -893,7 +897,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { - playbackInfo.positionUs = periodPositionUs; + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); resetRendererPosition(periodPositionUs); } @@ -1280,7 +1287,8 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { 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 af653ec2bd..dc703f924a 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 @@ -243,7 +243,7 @@ public interface Player { */ @Retention(RetentionPolicy.SOURCE) @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_INTERNAL}) + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may @@ -254,10 +254,15 @@ public interface Player { * Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; + /** + * Seek adjustment due to being unable to seek to the requested position or because the seek was + * permitted to be inexact. + */ + int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; /** * Discontinuity introduced internally by the source. */ - int DISCONTINUITY_REASON_INTERNAL = 2; + int DISCONTINUITY_REASON_INTERNAL = 3; /** * Register a listener to receive events from the player. The listener's methods will be called on From 74569bba450cc91b21771361f5e323b12ed1227b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 09:35:01 -0800 Subject: [PATCH 039/148] Don't do work after track selection when in ended state This causes the player to report that it's started loading when in the ended state. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176371892 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 56e16343ed..998779858b 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 @@ -896,7 +896,7 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (periodPositionUs != playbackInfo.positionUs) { + if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, @@ -941,9 +941,11 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - maybeContinueLoading(); - updatePlaybackPositions(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + if (state != Player.STATE_ENDED) { + maybeContinueLoading(); + updatePlaybackPositions(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } } private boolean isTimelineReady(long playingPeriodDurationUs) { From 56c1c3f6a724b62ccb624148e2c892a4ef9519bf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 22 Nov 2017 17:59:36 +0000 Subject: [PATCH 040/148] Revert "Make ExtractorMediaSource timeline dynamic until duration is set" This reverts commit b688a562508e74721841aaaa770e9dc47bd378dd. --- .../source/ClippingMediaSourceTest.java | 10 +-- .../source/ExtractorMediaSource.java | 7 +- .../source/SinglePeriodTimeline.java | 23 +++--- .../source/SingleSampleMediaSource.java | 2 +- .../source/SinglePeriodTimelineTest.java | 77 ------------------- 5 files changed, 17 insertions(+), 102 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java 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 0fefff86a2..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 @@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 17f2fc2dad..1b3f6cb95c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -204,11 +204,8 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // If the duration is currently unset, we expect to be able to update the window when its - // duration eventually becomes known. - boolean isDynamic = timelineDurationUs == C.TIME_UNSET; - sourceListener.onSourceInfoRefreshed(this, - new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); + sourceListener.onSourceInfoRefreshed( + this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 9cce67f68c..6f35438444 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline containing a single period and a window that spans it. + * Creates a timeline of one period of known duration, and a static window starting at zero and + * extending to that duration. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. - * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); + public SinglePeriodTimeline(long durationUs, boolean isSeekable) { + this(durationUs, durationUs, 0, 0, isSeekable, false); } /** @@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,16 +106,11 @@ public final class SinglePeriodTimeline extends Timeline { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic && defaultPositionProjectionUs != 0) { - if (windowDurationUs == C.TIME_UNSET) { - // Don't allow projection into a window that has an unknown duration. + if (isDynamic) { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the live window. windowDefaultStartPositionUs = C.TIME_UNSET; - } else { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the window. - windowDefaultStartPositionUs = C.TIME_UNSET; - } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index ede2c7ccf9..dd901958fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -110,7 +110,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true, false); + timeline = new SinglePeriodTimeline(durationUs, true); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java deleted file mode 100644 index 94ca8b03f0..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java +++ /dev/null @@ -1,77 +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; - -import static com.google.common.truth.Truth.assertThat; - -import android.util.Pair; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.Timeline.Window; -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 SinglePeriodTimeline}. - */ -@RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) -public final class SinglePeriodTimelineTest { - - private Window window; - private Period period; - - @Before - public void setUp() throws Exception { - window = new Window(); - period = new Period(); - } - - @Test - public void testGetPeriodPositionDynamicWindowUnknownDuration() { - SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); - // Should return null with any positive position projection. - Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); - assertThat(position).isNull(); - // Should return (0, 0) without a position projection. - position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); - assertThat(position.first).isEqualTo(0); - assertThat(position.second).isEqualTo(0); - } - - @Test - public void testGetPeriodPositionDynamicWindowKnownDuration() { - long windowDurationUs = 1000; - SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, - 0, false, true); - // Should return null with a positive position projection beyond window duration. - Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, - windowDurationUs + 1); - assertThat(position).isNull(); - // Should return (0, duration) with a projection equal to window duration. - position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); - assertThat(position.first).isEqualTo(0); - assertThat(position.second).isEqualTo(windowDurationUs); - // Should return (0, 0) without a position projection. - position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); - assertThat(position.first).isEqualTo(0); - assertThat(position.second).isEqualTo(0); - } - -} From 75b90625839a548124d2f90788b790b3bcd21579 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 22 Nov 2017 18:06:14 +0000 Subject: [PATCH 041/148] Send discontinuity at adjustments after shuffle/repeat mode changes. --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 998779858b..4d1767b64c 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 @@ -474,8 +474,12 @@ import java.io.IOException; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, - playbackInfo.contentPositionUs); + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); + } } } From 3562fe1c69c8a085ff5c92449a6d4b2ff95a133e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 22 Nov 2017 20:38:53 +0000 Subject: [PATCH 042/148] SampleStream fixes --- .../source/ExtractorMediaPeriod.java | 11 ++++- .../source/chunk/ChunkSampleStream.java | 8 +++- .../source/hls/HlsSampleStreamWrapper.java | 41 +++++++++++++------ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index c418c427f7..1228061cde 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -361,7 +361,7 @@ import java.util.Arrays; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample()); + return !suppressRead() && (loadingFinished || sampleQueues[track].hasNextSample()); } /* package */ void maybeThrowError() throws IOException { @@ -370,7 +370,7 @@ import java.util.Arrays; /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { - if (notifyDiscontinuity || isPendingReset()) { + if (suppressRead()) { return C.RESULT_NOTHING_READ; } return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, @@ -378,6 +378,9 @@ import java.util.Arrays; } /* package */ int skipData(int track, long positionUs) { + if (suppressRead()) { + return 0; + } SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); @@ -387,6 +390,10 @@ import java.util.Arrays; } } + private boolean suppressRead() { + return notifyDiscontinuity || isPendingReset(); + } + // Loader.Callback implementation. @Override 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 8a9be92d75..bb51ae074e 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 @@ -272,9 +272,11 @@ public class ChunkSampleStream implements SampleStream, S @Override public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; + } int skipCount; if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.advanceToEnd(); skipCount = primarySampleQueue.advanceToEnd(); } else { skipCount = primarySampleQueue.advanceTo(positionUs, true, true); @@ -282,7 +284,9 @@ public class ChunkSampleStream implements SampleStream, S skipCount = 0; } } - primarySampleQueue.discardToRead(); + if (skipCount > 0) { + primarySampleQueue.discardToRead(); + } return skipCount; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 3eae83624b..ddd6689fa6 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -385,7 +385,35 @@ import java.util.LinkedList; if (isPendingReset()) { return C.RESULT_NOTHING_READ; } + int result = sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, + loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + discardToRead(); + } + return result; + } + public int skipData(int trackGroupIndex, long positionUs) { + if (isPendingReset()) { + return 0; + } + int skipCount; + SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + skipCount = sampleQueue.advanceToEnd(); + } else { + skipCount = sampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } + } + if (skipCount > 0) { + discardToRead(); + } + return skipCount; + } + + private void discardToRead() { if (!mediaChunks.isEmpty()) { while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { mediaChunks.removeFirst(); @@ -399,19 +427,6 @@ import java.util.LinkedList; } downstreamTrackFormat = trackFormat; } - - return sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, loadingFinished, - lastSeekPositionUs); - } - - public int skipData(int trackGroupIndex, long positionUs) { - SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; - if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - return sampleQueue.advanceToEnd(); - } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; - } } private boolean finishedReadingChunk(HlsMediaChunk chunk) { From 1b66908f7d79b6891d8d99705470797640457e72 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Nov 2017 01:50:47 -0800 Subject: [PATCH 043/148] Update version strings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176989632 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3902ec5cbd..ecfe3eb96f 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,18 @@ Next add a gradle compile dependency to the `build.gradle` file of your app module. The following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `r2.X.X` is your preferred version. Alternatively, you can depend on only +where `2.X.X` is your preferred version. Alternatively, you can depend on only the library modules that you actually need. For example the following will add dependencies on the Core, DASH and UI library modules, as might be required for an app that plays DASH content: ```gradle -compile 'com.google.android.exoplayer:exoplayer-core:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-dash:r2.X.X' -compile 'com.google.android.exoplayer:exoplayer-ui:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer-core:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` The available library modules are listed below. Adding a dependency to the full From 566170f308d2e29d7ee568f98be3344a1a3743ed Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 Nov 2017 08:55:50 -0800 Subject: [PATCH 044/148] Add javadoc to ExoPlayerTestRunner. Someone must have forgotten to do this when rewriting this class. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175296249 --- .../testutil/ExoPlayerTestRunner.java | 167 +++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) 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 a87066415d..a1f8fc7861 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 @@ -62,14 +62,30 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public interface PlayerFactory { + /** + * Creates a new {@link SimpleExoPlayer} using the provided renderers factory, track selector, + * and load control. + * + * @param renderersFactory A {@link RenderersFactory} to be used for the new player. + * @param trackSelector A {@link MappingTrackSelector} to be used for the new player. + * @param loadControl A {@link LoadControl} to be used for the new player. + * @return A new {@link SimpleExoPlayer}. + */ SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, MappingTrackSelector trackSelector, LoadControl loadControl); } + /** + * A generic video {@link Format} which can be used to set up media sources and renderers. + */ public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); + + /** + * A generic audio {@link Format} which can be used to set up media sources and renderers. + */ public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -85,19 +101,45 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ActionSchedule actionSchedule; private Player.EventListener eventListener; + /** + * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The + * default value is a non-seekable, non-dynamic {@link FakeTimeline} with zero duration. Setting + * the timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * + * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test + * runner. + * @return This builder. + */ public Builder setTimeline(Timeline timeline) { Assert.assertNull(mediaSource); this.timeline = timeline; return this; } + /** + * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value + * is null. Setting the manifest is not allowed after a call to + * {@link #setMediaSource(MediaSource)}. + * + * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. + * @return This builder. + */ public Builder setManifest(Object manifest) { Assert.assertNull(mediaSource); this.manifest = manifest; return this; } - /** Replaces {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. */ + /** + * Sets a {@link MediaSource} to be used by the test runner. The default value is a + * {@link FakeMediaSource} with the timeline and manifest provided by + * {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. Setting the media source is + * not allowed after calls to {@link #setTimeline(Timeline)} and/or + * {@link #setManifest(Object)}. + * + * @param mediaSource A {@link MediaSource} to be used by the test runner. + * @return This builder. + */ public Builder setMediaSource(MediaSource mediaSource) { Assert.assertNull(timeline); Assert.assertNull(manifest); @@ -105,49 +147,118 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Sets a {@link MappingTrackSelector} to be used by the test runner. The default value is a + * {@link DefaultTrackSelector}. + * + * @param trackSelector A {@link MappingTrackSelector} to be used by the test runner. + * @return This builder. + */ public Builder setTrackSelector(MappingTrackSelector trackSelector) { this.trackSelector = trackSelector; return this; } + /** + * Sets a {@link LoadControl} to be used by the test runner. The default value is a + * {@link DefaultLoadControl}. + * + * @param loadControl A {@link LoadControl} to be used by the test runner. + * @return This builder. + */ public Builder setLoadControl(LoadControl loadControl) { this.loadControl = loadControl; return this; } + /** + * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media + * periods and for setting up a {@link FakeRenderer}. The default value is a single + * {@link #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media + * source with {@link #setMediaSource(MediaSource)} and renderers with + * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. + * + * @param supportedFormats A list of supported {@link Format}s. + * @return This builder. + */ public Builder setSupportedFormats(Format... supportedFormats) { this.supportedFormats = supportedFormats; return this; } + /** + * Sets the {@link Renderer}s to be used by the test runner. The default value is a single + * {@link FakeRenderer} supporting the formats set by {@link #setSupportedFormats(Format...)}. + * Setting the renderers is not allowed after a call to + * {@link #setRenderersFactory(RenderersFactory)}. + * + * @param renderers A list of {@link Renderer}s to be used by the test runner. + * @return This builder. + */ public Builder setRenderers(Renderer... renderers) { Assert.assertNull(renderersFactory); this.renderers = renderers; return this; } - /** Replaces {@link #setRenderers(Renderer...)}. */ + /** + * Sets the {@link RenderersFactory} to be used by the test runner. The default factory creates + * all renderers set by {@link #setRenderers(Renderer...)}. Setting the renderer factory is not + * allowed after a call to {@link #setRenderers(Renderer...)}. + * + * @param renderersFactory A {@link RenderersFactory} to be used by the test runner. + * @return This builder. + */ public Builder setRenderersFactory(RenderersFactory renderersFactory) { Assert.assertNull(renderers); this.renderersFactory = renderersFactory; return this; } + /** + * Sets the {@link PlayerFactory} which creates the {@link SimpleExoPlayer} to be used by the + * test runner. The default value is a {@link SimpleExoPlayer} with the renderers provided by + * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)}, the + * track selector provided by {@link #setTrackSelector(MappingTrackSelector)} and the load + * control provided by {@link #setLoadControl(LoadControl)}. + * + * @param playerFactory A {@link PlayerFactory} to create the player. + * @return This builder. + */ public Builder setExoPlayer(PlayerFactory playerFactory) { this.playerFactory = playerFactory; return this; } + /** + * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be + * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. + * + * @param actionSchedule An {@link ActionSchedule} to be used by the test runner. + * @return This builder. + */ public Builder setActionSchedule(ActionSchedule actionSchedule) { this.actionSchedule = actionSchedule; return this; } + /** + * Sets an {@link Player.EventListener} to be registered to listen to player events. + * + * @param eventListener A {@link Player.EventListener} to be registered by the test runner to + * listen to player events. + * @return This builder. + */ public Builder setEventListener(Player.EventListener eventListener) { this.eventListener = eventListener; return this; } + /** + * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults. + * + * @return The built {@link ExoPlayerTestRunner}. + */ public ExoPlayerTestRunner build() { if (supportedFormats == null) { supportedFormats = new Format[] { VIDEO_FORMAT }; @@ -234,6 +345,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Called on the test thread to run the test. + /** + * Starts the test runner on its own thread. This will trigger the creation of the player, the + * listener registration, the start of the action schedule, and the preparation of the player + * with the provided media source. + * + * @return This test runner. + */ public ExoPlayerTestRunner start() { handler.post(new Runnable() { @Override @@ -257,6 +375,16 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Blocks the current thread until the test runner finishes. A test is deemed to be finished when + * the playback state transitions to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE}, or + * when am {@link ExoPlaybackException} is thrown. + * + * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed + * the method will throw a {@link TimeoutException}. + * @return This test runner. + * @throws Exception If any exception occurred during playback, release, or due to a timeout. + */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); @@ -271,6 +399,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Assertions called on the test thread after test finished. + /** + * Asserts that the timelines reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * timelines. + * + * @param timelines A list of expected {@link Timeline}s. + */ public void assertTimelinesEqual(Timeline... timelines) { Assert.assertEquals(timelines.length, this.timelines.size()); for (Timeline timeline : timelines) { @@ -278,6 +413,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the manifests reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * manifest. + * + * @param manifests A list of expected manifests. + */ public void assertManifestsEqual(Object... manifests) { Assert.assertEquals(manifests.length, this.manifests.size()); for (Object manifest : manifests) { @@ -285,14 +427,35 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the last track group array reported by + * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to + * the provided track group array. + * + * @param trackGroupArray The expected {@link TrackGroupArray}. + */ public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { Assert.assertEquals(trackGroupArray, this.trackGroups); } + /** + * Asserts that the number of reported discontinuities by + * {@link Player.EventListener#onPositionDiscontinuity(int)} is equal to the provided number. + * + * @param expectedCount The expected number of position discontinuities. + */ public void assertPositionDiscontinuityCount(int expectedCount) { Assert.assertEquals(expectedCount, positionDiscontinuityCount); } + /** + * Asserts that the indices of played periods is equal to the provided list of periods. A period + * is considered to be played if it was the current period after a position discontinuity or a + * media source preparation. When the same period is repeated automatically due to enabled repeat + * modes, it is reported twice. Seeks within the current period are not reported. + * + * @param periodIndices A list of expected period indices. + */ public void assertPlayedPeriodIndices(int... periodIndices) { Assert.assertEquals(periodIndices.length, this.periodIndices.size()); for (int periodIndex : periodIndices) { From bff221b85e365733cbc9bcf630fc5b27efcfe3c6 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 07:28:57 -0800 Subject: [PATCH 045/148] Introduce Builder pattern to create MediaSource. Start with DASH MediaSource. The number of injected arguments is getting out-of-control. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175529031 --- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 147 ++++++++++++++++++ .../playbacktests/gts/DashTestRunner.java | 7 +- 3 files changed, 157 insertions(+), 4 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 08c5bddb09..614626077a 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 @@ -365,8 +365,11 @@ public class PlayerActivity extends Activity implements OnClickListener, return new SsMediaSource(uri, buildDataSourceFactory(false), new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); case C.TYPE_DASH: - return new DashMediaSource(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return DashMediaSource.Builder + .forManifestUri(uri, buildDataSourceFactory(false), + new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_HLS: return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); case C.TYPE_OTHER: diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index c529fcab4b..3d5a9c393d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -114,6 +115,142 @@ public final class DashMediaSource implements MediaSource { private int firstPeriodId; + /** + * Builder for {@link DashMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final DashManifest manifest; + private final Uri manifestUri; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; + + private ParsingLoadable.Parser manifestParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + + private int minLoadableRetryCount; + private long livePresentationDelayMs; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @return A new builder. + */ + public static Builder forSideLoadedManifest(DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory) { + return new Builder(manifest, null, null, chunkSourceFactory); + } + + /** + * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @return A new builder. + */ + public static Builder forManifestUri(Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { + return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); + } + + private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory) { + this.manifest = manifest; + this.manifestUri = manifestUri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the duration in milliseconds by which the default start position should precede the end + * of the live window for live playbacks. The default value is + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. Use + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by + * the manifest, if present. + * @return This builder. + */ + public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + this.livePresentationDelayMs = livePresentationDelayMs; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the manifest parser to parse loaded manifest data. The default is + * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * + * @param manifestParser A parser for loaded manifest data. + * @return This builder. + */ + public Builder setManifestParser( + ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + + /** + * Builds a new {@link DashMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link DashMediaSource}. + */ + public DashMediaSource build() { + Assertions.checkArgument(manifest == null || !manifest.dynamic); + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + boolean loadableManifestUri = manifestUri != null; + if (loadableManifestUri && manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, + chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + eventListener); + } + + } + /** * Constructs an instance to play a given {@link DashManifest}, which must be static. * @@ -121,7 +258,9 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, @@ -136,7 +275,9 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -154,7 +295,9 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -178,7 +321,9 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, @@ -203,7 +348,9 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 85cefbc2f6..215d8a0518 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,8 +316,11 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + return DashMediaSource.Builder + .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) + .setLivePresentationDelayMs(0) + .build(); } @Override From 877c89a0e1f40b08c624ab5b0c2cb1bba3836846 Mon Sep 17 00:00:00 2001 From: arnaudberry Date: Mon, 13 Nov 2017 10:49:09 -0800 Subject: [PATCH 046/148] Make it possible to extend DashManifestParser to parse revision-id. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175554723 --- .../source/dash/manifest/DashManifestParser.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 7ffb429784..137e29c5ab 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 @@ -488,7 +488,7 @@ public class DashManifestParser extends DefaultHandler segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, - inbandEventStreams); + inbandEventStreams, Representation.REVISION_ID_DEFAULT); } protected Format buildFormat(String id, String containerMimeType, int width, int height, @@ -535,7 +535,7 @@ public class DashManifestParser extends DefaultHandler } ArrayList inbandEventStreams = representationInfo.inbandEventStreams; inbandEventStreams.addAll(extraInbandEventStreams); - return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, + return Representation.newInstance(contentId, representationInfo.revisionId, format, representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStreams); } @@ -986,7 +986,8 @@ public class DashManifestParser extends DefaultHandler } } - private static final class RepresentationInfo { + /** A parsed Representation element. */ + protected static final class RepresentationInfo { public final Format format; public final String baseUrl; @@ -994,16 +995,18 @@ public class DashManifestParser extends DefaultHandler public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; + public final long revisionId; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams) { + ArrayList inbandEventStreams, long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; this.inbandEventStreams = inbandEventStreams; + this.revisionId = revisionId; } } From 5bf4c249a28b13acb06f8cd27db5fbe497237cc8 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 11:10:54 -0800 Subject: [PATCH 047/148] Notify TrackSelection when it's enabled and disabled. Add onEnable() and onDisable() call-backs to TrackSelection. This allows TrackSelection to perform interesting operations (like subscribe to NetworkStatus) and clean up after itself. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175558485 --- .../android/exoplayer2/ExoPlayerTest.java | 141 ++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 33 +++- .../trackselection/BaseTrackSelection.java | 10 ++ .../trackselection/TrackSelection.java | 20 ++- .../testutil/FakeTrackSelection.java | 132 ++++++++++++++++ .../testutil/FakeTrackSelector.java | 86 +++++++++++ 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 56d5f05d00..0edd19bc09 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 @@ -28,6 +28,8 @@ 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 com.google.android.exoplayer2.testutil.FakeTrackSelection; +import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -311,4 +313,143 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made once (1 period). + // Track selections are not reused, so there are 2 track selections made. + assertEquals(2, createdTrackSelections.size()); + // There should be 2 track selections enabled in total. + assertEquals(2, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), + new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice (2 periods). + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // There should be 4 track selections enabled in total. + assertEquals(4, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // TrackSelections are reused, so there are only 2 track selections made for 2 renderers. + assertEquals(2, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + } 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 4d1767b64c..33889a2b57 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 @@ -1666,11 +1666,11 @@ import java.io.IOException; // Undo the effect of previous call to associate no-sample renderers with empty tracks // so the mediaPeriod receives back whatever it sent us before. disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); + updatePeriodTrackSelectorResult(trackSelectorResult); // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); associateNoSampleRenderersWithEmptySampleStream(sampleStreams); - periodTrackSelectorResult = trackSelectorResult; // Update whether we have enabled tracks and sanity check the expected streams are non-null. hasEnabledTracks = false; @@ -1692,6 +1692,7 @@ import java.io.IOException; } public void release() { + updatePeriodTrackSelectorResult(null); try { if (info.endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); @@ -1704,6 +1705,36 @@ import java.io.IOException; } } + private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + if (periodTrackSelectorResult != null) { + disableTrackSelectionsInResult(periodTrackSelectorResult); + } + periodTrackSelectorResult = trackSelectorResult; + if (periodTrackSelectorResult != null) { + enableTrackSelectionsInResult(periodTrackSelectorResult); + } + } + + private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.enable(); + } + } + } + + private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.disable(); + } + } + } + /** * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy * {@link EmptySampleStream} that was associated with it. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 054ee7973f..6bc6afb88b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -78,6 +78,16 @@ public abstract class BaseTrackSelection implements TrackSelection { blacklistUntilTimes = new long[length]; } + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + @Override public final TrackGroup getTrackGroup() { return group; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index ad02b6c775..027b2abde9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -47,6 +47,20 @@ public interface TrackSelection { } + /** + * Enables the track selection. + *

      + * This method may not be called when the track selection is already enabled. + */ + void enable(); + + /** + * Disables this track selection. + *

      + * This method may only be called when the track selection is already enabled. + */ + void disable(); + /** * Returns the {@link TrackGroup} to which the selected tracks belong. */ @@ -124,6 +138,8 @@ public interface TrackSelection { /** * Updates the selected track. + *

      + * This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -150,7 +166,7 @@ public interface TrackSelection { * An example of a case where a smaller value may be returned is if network conditions have * improved dramatically, allowing chunks to be discarded and re-buffered in a track of * significantly higher quality. Discarding chunks may allow faster switching to a higher quality - * track in this case. + * track in this case. This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -167,6 +183,8 @@ public interface TrackSelection { * period of time. Blacklisting will fail if all other tracks are currently blacklisted. If * blacklisting the currently selected track, note that it will remain selected until the next * call to {@link #updateSelectedTrack(long, long, long)}. + *

      + * This method may only be called when the selection is enabled. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java new file mode 100644 index 0000000000..20346a0355 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.List; +import junit.framework.Assert; + +/** + * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number + * of calls to its methods. + */ +public final class FakeTrackSelection implements TrackSelection { + + private final TrackGroup rendererTrackGroup; + + public int enableCount; + public int releaseCount; + public boolean isEnabled; + + public FakeTrackSelection(TrackGroup rendererTrackGroup) { + this.rendererTrackGroup = rendererTrackGroup; + } + + @Override + public void enable() { + // assert that track selection is in disabled state before this call. + Assert.assertFalse(isEnabled); + enableCount++; + isEnabled = true; + } + + @Override + public void disable() { + // assert that track selection is in enabled state before this call. + Assert.assertTrue(isEnabled); + releaseCount++; + isEnabled = false; + } + + @Override + public TrackGroup getTrackGroup() { + return rendererTrackGroup; + } + + @Override + public int length() { + return rendererTrackGroup.length; + } + + @Override + public Format getFormat(int index) { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getIndexInTrackGroup(int index) { + return 0; + } + + @Override + public int indexOf(Format format) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public int indexOf(int indexInTrackGroup) { + return 0; + } + + @Override + public Format getSelectedFormat() { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getSelectedIndexInTrackGroup() { + return 0; + } + + @Override + public int getSelectedIndex() { + return 0; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_UNKNOWN; + } + + @Override + public Object getSelectionData() { + return null; + } + + @Override + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { + Assert.assertTrue(isEnabled); + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public boolean blacklist(int index, long blacklistDurationMs) { + Assert.assertTrue(isEnabled); + return false; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java new file mode 100644 index 0000000000..da9a1a18ad --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.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.testutil; + +import android.support.annotation.NonNull; +import com.google.android.exoplayer2.ExoPlaybackException; +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.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.ArrayList; +import java.util.List; + +/** + * A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. + */ +public class FakeTrackSelector extends MappingTrackSelector { + + private final List selectedTrackSelections = new ArrayList<>(); + private final boolean mayReuseTrackSelection; + + public FakeTrackSelector() { + this(false); + } + + /** + * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse + * {@link TrackSelection}s during track selection, when it finds previously-selected track + * selection using the same {@link TrackGroup}. + */ + public FakeTrackSelector(boolean mayReuseTrackSelection) { + this.mayReuseTrackSelection = mayReuseTrackSelection; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + List resultList = new ArrayList<>(); + for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { + TrackGroup trackGroup = trackGroupArray.get(0); + FakeTrackSelection trackSelectionForRenderer = reuseOrCreateTrackSelection(trackGroup); + resultList.add(trackSelectionForRenderer); + } + return resultList.toArray(new TrackSelection[resultList.size()]); + } + + @NonNull + private FakeTrackSelection reuseOrCreateTrackSelection(TrackGroup trackGroup) { + FakeTrackSelection trackSelectionForRenderer = null; + if (mayReuseTrackSelection) { + for (FakeTrackSelection selectedTrackSelection : selectedTrackSelections) { + if (selectedTrackSelection.getTrackGroup().equals(trackGroup)) { + trackSelectionForRenderer = selectedTrackSelection; + } + } + } + if (trackSelectionForRenderer == null) { + trackSelectionForRenderer = new FakeTrackSelection(trackGroup); + selectedTrackSelections.add(trackSelectionForRenderer); + } + return trackSelectionForRenderer; + } + + /** + * Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. + */ + public List getSelectedTrackSelections() { + return selectedTrackSelections; + } + +} From 79a9155438d92e459cd320b2f540e34cd029f325 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 07:24:10 -0700 Subject: [PATCH 048/148] Add support for 608/708 captions in HLS+fMP4 This also allows exposing multiple CC channels to any fMP4 extractor client. Issue:#1661 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174458725 --- .../mp4/FragmentedMp4ExtractorTest.java | 23 ++++---- .../extractor/mp4/FragmentedMp4Extractor.java | 54 ++++++++++++------- .../source/dash/DefaultDashChunkSource.java | 11 ++-- .../hls/DefaultHlsExtractorFactory.java | 3 +- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index c9364aa605..d24788f74a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -16,9 +16,13 @@ package com.google.android.exoplayer2.extractor.mp4; import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.Format; 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.util.MimeTypes; +import java.util.Collections; +import java.util.List; /** * Unit test for {@link FragmentedMp4Extractor}. @@ -26,26 +30,23 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", - getInstrumentation()); + ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.emptyList()), + "mp4/sample_fragmented.mp4", getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorAsserts.assertBehavior( - getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), - "mp4/sample_fragmented_sei.mp4", getInstrumentation()); + ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); + ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4", + getInstrumentation()); } - private static ExtractorFactory getExtractorFactory() { - return getExtractorFactory(0); - } - - private static ExtractorFactory getExtractorFactory(final int flags) { + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return new ExtractorFactory() { @Override public Extractor create() { - return new FragmentedMp4Extractor(flags, null); + return new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } }; } 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 867e4501fa..e86157dd92 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 @@ -46,6 +46,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Stack; @@ -73,8 +74,8 @@ public final class FragmentedMp4Extractor implements Extractor { */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, - FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -93,20 +94,15 @@ public final class FragmentedMp4Extractor implements Extractor { * messages in the stream will be delivered as samples to this track. */ public static final int FLAG_ENABLE_EMSG_TRACK = 4; - /** - * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages - * contained within SEI NAL units in the stream will be delivered as samples to this track. - */ - public static final int FLAG_ENABLE_CEA608_TRACK = 8; /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 16; + private static final int FLAG_SIDELOADED = 8; /** * Flag to ignore any edit lists in the stream. */ - public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32; + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16; private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); @@ -124,7 +120,8 @@ public final class FragmentedMp4Extractor implements Extractor { @Flags private final int flags; private final Track sideloadedTrack; - // Manifest DRM data. + // Sideloaded data. + private final List closedCaptionFormats; private final DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. @@ -193,15 +190,33 @@ public final class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { + this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, + Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + */ + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; this.sideloadedDrmInitData = sideloadedDrmInitData; + this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -483,12 +498,13 @@ public final class FragmentedMp4Extractor implements Extractor { eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { - TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, - C.TRACK_TYPE_TEXT); - cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, - null)); - cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; + if (cea608TrackOutputs == null) { + cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; + for (int i = 0; i < cea608TrackOutputs.length; i++) { + TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); + output.format(closedCaptionFormats.get(i)); + cea608TrackOutputs[i] = output; + } } } @@ -1123,7 +1139,7 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - processSeiNalUnitPayload = cea608TrackOutputs != null + processSeiNalUnitPayload = cea608TrackOutputs.length > 0 && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; 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 1eac1b5616..66455b2f04 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 @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -424,10 +425,12 @@ public class DefaultDashChunkSource implements DashChunkSource { if (enableEventMessageTrack) { flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; } - if (enableCea608Track) { - flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; - } - extractor = new FragmentedMp4Extractor(flags); + // TODO: Use caption format information from the manifest if available. + List closedCaptionFormats = enableCea608Track + ? Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) + : Collections.emptyList(); + extractor = new FragmentedMp4Extractor(flags, null, null, null, closedCaptionFormats); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 957aefcdbc..c801520927 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -72,7 +72,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); + extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { // For any other file extension, we assume TS format. @DefaultTsPayloadReaderFactory.Flags From 25dd8aa1f8c173563d114e5a55f9c08cbd60b840 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 13 Nov 2017 13:18:42 -0800 Subject: [PATCH 049/148] Fix cenc mode support and add support for the .mp4a extension. Also add encrypted HLS internal sample streams. Issue:#1661 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175577648 --- .../exoplayer2/source/hls/DefaultHlsExtractorFactory.java | 4 +++- .../exoplayer2/source/hls/playlist/HlsPlaylistParser.java | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index c801520927..dc838c9506 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -43,6 +43,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { public static final String MP3_FILE_EXTENSION = ".mp3"; public static final String MP4_FILE_EXTENSION = ".mp4"; public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; + public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4"; public static final String VTT_FILE_EXTENSION = ".vtt"; public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -71,7 +72,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // Only reuse TS and fMP4 extractors. extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) - || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) + || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index c63ded6275..90644125b1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -107,7 +107,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Tue, 14 Nov 2017 03:48:47 -0800 Subject: [PATCH 050/148] Continue adding Builder to MediaSource. Add Builder pattern to SsMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175659618 --- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 117 +++++++------- .../source/smoothstreaming/SsMediaSource.java | 143 ++++++++++++++++++ 3 files changed, 206 insertions(+), 61 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 614626077a..65e1c0e083 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 @@ -362,8 +362,11 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return new SsMediaSource(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return SsMediaSource.Builder + .forManifestUri(uri, buildDataSourceFactory(false), + new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_DASH: return DashMediaSource.Builder .forManifestUri(uri, buildDataSourceFactory(false), diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 3d5a9c393d..54a5086d3b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -58,63 +58,6 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - /** - * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the - * duration by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; - /** - * A fixed default presentation delay for live streams. The presentation delay is the duration - * by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; - - /** - * The interval in milliseconds between invocations of - * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). - */ - private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; - /** - * The minimum default start position for live streams, relative to the start of the live window. - */ - private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; - - private static final String TAG = "DashMediaSource"; - - private final boolean sideloadedManifest; - private final DataSource.Factory manifestDataSourceFactory; - private final DashChunkSource.Factory chunkSourceFactory; - private final int minLoadableRetryCount; - private final long livePresentationDelayMs; - private final EventDispatcher eventDispatcher; - private final ParsingLoadable.Parser manifestParser; - private final ManifestCallback manifestCallback; - private final Object manifestUriLock; - private final SparseArray periodsById; - private final Runnable refreshManifestRunnable; - private final Runnable simulateManifestRefreshRunnable; - - private Listener sourceListener; - private DataSource dataSource; - private Loader loader; - private LoaderErrorThrower loaderErrorThrower; - - private Uri manifestUri; - private long manifestLoadStartTimestamp; - private long manifestLoadEndTimestamp; - private DashManifest manifest; - private Handler handler; - private long elapsedRealtimeOffsetMs; - - private int firstPeriodId; - /** * Builder for {@link DashMediaSource}. Each builder instance can only be used once. */ @@ -142,6 +85,7 @@ public final class DashMediaSource implements MediaSource { */ public static Builder forSideLoadedManifest(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory) { + Assertions.checkArgument(!manifest.dynamic); return new Builder(manifest, null, null, chunkSourceFactory); } @@ -227,7 +171,6 @@ public final class DashMediaSource implements MediaSource { return this; } - /** * Builds a new {@link DashMediaSource} using the current parameters. *

      @@ -236,7 +179,6 @@ public final class DashMediaSource implements MediaSource { * @return The newly built {@link DashMediaSource}. */ public DashMediaSource build() { - Assertions.checkArgument(manifest == null || !manifest.dynamic); Assertions.checkArgument((eventListener == null) == (eventHandler == null)); Assertions.checkState(!isBuildCalled); isBuildCalled = true; @@ -251,6 +193,63 @@ public final class DashMediaSource implements MediaSource { } + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** + * A constant indicating that the presentation delay for live streams should be set to + * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the + * duration by which the default start position precedes the end of the live window. + */ + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; + /** + * A fixed default presentation delay for live streams. The presentation delay is the duration + * by which the default start position precedes the end of the live window. + */ + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; + + /** + * The interval in milliseconds between invocations of + * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the + * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). + */ + private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; + /** + * The minimum default start position for live streams, relative to the start of the live window. + */ + private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; + + private static final String TAG = "DashMediaSource"; + + private final boolean sideloadedManifest; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; + private final int minLoadableRetryCount; + private final long livePresentationDelayMs; + private final EventDispatcher eventDispatcher; + private final ParsingLoadable.Parser manifestParser; + private final ManifestCallback manifestCallback; + private final Object manifestUriLock; + private final SparseArray periodsById; + private final Runnable refreshManifestRunnable; + private final Runnable simulateManifestRefreshRunnable; + + private Listener sourceListener; + private DataSource dataSource; + private Loader loader; + private LoaderErrorThrower loaderErrorThrower; + + private Uri manifestUri; + private long manifestLoadStartTimestamp; + private long manifestLoadEndTimestamp; + private DashManifest manifest; + private Handler handler; + private long elapsedRealtimeOffsetMs; + + private int firstPeriodId; + /** * Constructs an instance to play a given {@link DashManifest}, which must be static. * diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 548f787741..5a93847428 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -51,6 +52,138 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } + /** + * Builder for {@link SsMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final SsManifest manifest; + private final Uri manifestUri; + private final DataSource.Factory manifestDataSourceFactory; + private final SsChunkSource.Factory chunkSourceFactory; + + private ParsingLoadable.Parser manifestParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + + private int minLoadableRetryCount; + private long livePresentationDelayMs; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @return A new builder. + */ + public static Builder forSideLoadedManifest(SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory) { + Assertions.checkArgument(!manifest.isLive); + return new Builder(manifest, null, null, chunkSourceFactory); + } + + /** + * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @return A new builder. + */ + public static Builder forManifestUri(Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { + return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); + } + + private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory) { + this.manifest = manifest; + this.manifestUri = manifestUri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the duration in milliseconds by which the default start position should precede the end + * of the live window for live playbacks. The default value is + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. + * @return This builder. + */ + public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + this.livePresentationDelayMs = livePresentationDelayMs; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the manifest parser to parse loaded manifest data. The default is an instance of + * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * + * @param manifestParser A parser for loaded manifest data. + * @return This builder. + */ + public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + /** + * Builds a new {@link SsMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link SsMediaSource}. + */ + public SsMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + boolean loadableManifestUri = manifestUri != null; + if (loadableManifestUri && manifestParser == null) { + manifestParser = new SsManifestParser(); + } + return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, + chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + eventListener); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -96,7 +229,9 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, @@ -111,7 +246,9 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -129,7 +266,9 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -151,7 +290,9 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, @@ -174,7 +315,9 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, From 77b48691aa0f5fc5218bfc9cb3d6cff4c8b06179 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 04:08:38 -0800 Subject: [PATCH 051/148] Suppress reference equality warning in EventLogger. We deliberately compare the track group returned by the track selection with the track group in the parameter to check if the track selection is referring to this particular track group. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175660909 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 27a5c68e28..9233b016f5 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -467,6 +467,9 @@ import java.util.Locale; } } + // Suppressing reference equality warning because the track group stored in the track selection + // must point to the exact track group object to be considered part of it. + @SuppressWarnings("ReferenceEquality") private static String getTrackStatusString(TrackSelection selection, TrackGroup group, int trackIndex) { return getTrackStatusString(selection != null && selection.getTrackGroup() == group From eea8cd169c76b52bcf303c137ceab60e6773c47f Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 06:17:02 -0800 Subject: [PATCH 052/148] Replaced the duplicated EMPTY track group array with the one already defined in TrackGroupArray. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175670266 --- .../android/exoplayer2/source/TrackGroupArray.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index 394cec891b..fb28da581c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -62,8 +62,11 @@ public final class TrackGroupArray { * @param group The group. * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(TrackGroup group) { for (int i = 0; i < length; i++) { + // Suppressed reference equality warning because this is looking for the index of a specific + // TrackGroup object, not the index of a potential equal TrackGroup. if (trackGroups[i] == group) { return i; } @@ -71,6 +74,13 @@ public final class TrackGroupArray { return C.INDEX_UNSET; } + /** + * Returns whether this track group array is empty. + */ + public boolean isEmpty() { + return length == 0; + } + @Override public int hashCode() { if (hashCode == 0) { From 76ba1890aa48202d46cbbeccc7b69286e272d263 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 08:06:50 -0800 Subject: [PATCH 053/148] Add method to FakeMediaSource to trigger source info refresh. This allows to remove the LazyMediaSource used within DynamicConcatenatingMediaSourceTest and also allows to write test which simulates dynamic timeline or manifest updates. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175680371 --- .../DynamicConcatenatingMediaSourceTest.java | 77 ++++++++----------- .../exoplayer2/testutil/FakeMediaSource.java | 38 +++++++-- 2 files changed, 61 insertions(+), 54 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 e506d0a4b3..e7b2a8d963 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 @@ -33,15 +33,12 @@ 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; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.Allocator; -import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -216,19 +213,26 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistWithLazyMediaSource() throws InterruptedException { timeline = null; - FakeMediaSource[] childSources = createMediaSources(2); - LazyMediaSource[] lazySources = new LazyMediaSource[4]; + + // Create some normal (immediately preparing) sources and some lazy sources whose timeline + // updates need to be triggered. + FakeMediaSource[] fastSources = createMediaSources(2); + FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { - lazySources[i] = new LazyMediaSource(); + lazySources[i] = new FakeMediaSource(null, null); } - //Add lazy sources before preparation + // Add lazy sources and normal sources before preparation. Also remove one lazy source again + // before preparation to check it doesn't throw or change the result. DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(lazySources[0]); - mediaSource.addMediaSource(0, childSources[0]); + mediaSource.addMediaSource(0, fastSources[0]); mediaSource.removeMediaSource(1); mediaSource.addMediaSource(1, lazySources[1]); assertNull(timeline); + + // Prepare and assert that the timeline contains all information for normal sources while having + // placeholder information for lazy sources. prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); assertNotNull(timeline); @@ -236,7 +240,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); - lazySources[1].triggerTimelineUpdate(createFakeTimeline(8)); + // Trigger source info refresh for lazy source and check that the timeline now contains all + // information for all windows. + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); @@ -244,10 +250,11 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, TIMEOUT_MS); - //Add lazy sources after preparation (and also try to prepare media period from lazy source). + // Add further lazy and normal sources after preparation. Also remove one lazy source again to + // check it doesn't throw or change the result. mediaSource.addMediaSource(1, lazySources[2]); waitForTimelineUpdate(); - mediaSource.addMediaSource(2, childSources[1]); + mediaSource.addMediaSource(2, fastSources[1]); waitForTimelineUpdate(); mediaSource.addMediaSource(0, lazySources[3]); waitForTimelineUpdate(); @@ -257,6 +264,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not + // called yet. MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); assertNotNull(lazyPeriod); final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); @@ -269,11 +278,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void onContinueLoadingRequested(MediaPeriod source) {} }, 0); assertFalse(lazyPeriodPrepared.block(1)); + // Assert that a second period can also be created and released without problems. MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); assertNotNull(secondLazyPeriod); mediaSource.releasePeriod(secondLazyPeriod); - lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); + // Trigger source info refresh for lazy media source. Assert that now all information is + // available again and the previously created period now also finished preparing. + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); @@ -281,9 +293,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); mediaSource.releasePeriod(lazyPeriod); + // Release media source and assert all normal and lazy media sources are fully released as well. mediaSource.releaseSource(); - childSources[0].assertReleased(); - childSources[1].assertReleased(); + for (FakeMediaSource fastSource : fastSources) { + fastSource.assertReleased(); + } + for (FakeMediaSource lazySource : lazySources) { + lazySource.assertReleased(); + } } public void testEmptyTimelineMediaSource() throws InterruptedException { @@ -662,38 +679,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } - private static class LazyMediaSource implements MediaSource { - - private Listener listener; - - public void triggerTimelineUpdate(Timeline timeline) { - listener.onSourceInfoRefreshed(this, timeline, null); - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.listener = listener; - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return new FakeMediaPeriod(TrackGroupArray.EMPTY); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - } - - @Override - public void releaseSource() { - } - - } - /** * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 1f2524110a..f4c8435801 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; @@ -34,28 +35,34 @@ import junit.framework.Assert; */ public class FakeMediaSource implements MediaSource { - protected final Timeline timeline; private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; private final ArrayList createdMediaPeriods; + protected Timeline timeline; private boolean preparedSource; private boolean releasedSource; + private Listener listener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a - * {@link TrackGroupArray} using the given {@link Format}s. + * {@link TrackGroupArray} using the given {@link Format}s. The provided {@link Timeline} may be + * null to prevent an immediate source info refresh message when preparing the media source. It + * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { + public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) { this(timeline, manifest, buildTrackGroupArray(formats)); } /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with the - * given {@link TrackGroupArray}. + * given {@link TrackGroupArray}. The provided {@link Timeline} may be null to prevent an + * immediate source info refresh message when preparing the media source. It can be manually set + * later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(Timeline timeline, Object manifest, TrackGroupArray trackGroupArray) { + public FakeMediaSource(@Nullable Timeline timeline, Object manifest, + TrackGroupArray trackGroupArray) { this.timeline = timeline; this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); @@ -67,7 +74,10 @@ public class FakeMediaSource implements MediaSource { public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assert.assertFalse(preparedSource); preparedSource = true; - listener.onSourceInfoRefreshed(this, timeline, manifest); + this.listener = listener; + if (timeline != null) { + listener.onSourceInfoRefreshed(this, timeline, manifest); + } } @Override @@ -77,9 +87,9 @@ public class FakeMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); Assert.assertTrue(preparedSource); Assert.assertFalse(releasedSource); + Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); createdMediaPeriods.add(id); @@ -103,11 +113,23 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + /** + * Sets a new timeline and manifest. If the source is already prepared, this triggers a source + * info refresh message being sent to the listener. + */ + public void setNewSourceInfo(Timeline newTimeline, Object manifest) { + Assert.assertFalse(releasedSource); + this.timeline = newTimeline; + if (preparedSource) { + listener.onSourceInfoRefreshed(this, timeline, manifest); + } + } + /** * Assert that the source and all periods have been released. */ public void assertReleased() { - Assert.assertTrue(releasedSource); + Assert.assertTrue(releasedSource || !preparedSource); } /** From 4bb5cda5f1c48075675c45ba49ff29057ab19f8f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 02:19:48 -0800 Subject: [PATCH 054/148] Some test cleanup The purpose of this change isn't to fix anything. It's just to simplify things a little bit. There will be following CLs that make some changes to get things onto correct threads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175800354 --- .../DynamicConcatenatingMediaSourceTest.java | 263 ++---------------- .../testutil/FakeSimpleExoPlayer.java | 47 +--- .../exoplayer2/testutil/OggTestData.java | 1 - .../exoplayer2/testutil/StubExoPlayer.java | 248 +++++++++++++++++ 4 files changed, 270 insertions(+), 289 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java 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 e7b2a8d963..96d11678c9 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,19 +15,14 @@ */ package com.google.android.exoplayer2.source; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import android.os.Looper; import android.os.Message; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod.Callback; @@ -37,8 +32,8 @@ 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.StubExoPlayer; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -456,7 +451,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -470,7 +465,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( @@ -485,7 +480,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), @@ -500,7 +495,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( @@ -514,7 +509,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); @@ -522,7 +517,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { }); waitForTimelineUpdate(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); @@ -535,7 +530,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( @@ -544,7 +539,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { }); waitForTimelineUpdate(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, @@ -585,24 +580,17 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() throws InterruptedException { + final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); handlerThread.start(); - Handler.Callback handlerCallback = Mockito.mock(Handler.Callback.class); - when(handlerCallback.handleMessage(any(Message.class))).thenReturn(false); - Handler handler = new Handler(handlerThread.getLooper(), handlerCallback); - final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - handler.post(new Runnable() { - @Override - public void run() { - prepareAndListenToTimelineUpdates(mediaSource); - } - }); - waitForTimelineUpdate(); + Handler handler = new Handler(handlerThread.getLooper()); return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); } private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { - mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { + mediaSource.prepareSource(new MessageHandlingExoPlayer(), true, new Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) { timeline = newTimeline; @@ -669,244 +657,34 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private static class DynamicConcatenatingMediaSourceAndHandler { public final DynamicConcatenatingMediaSource mediaSource; - public final Handler handler; + public final Handler mainHandler; public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler handler) { + Handler mainHandler) { this.mediaSource = mediaSource; - this.handler = handler; + this.mainHandler = mainHandler; } } /** - * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. + * ExoPlayer that only accepts custom messages and runs them on a separate handler thread. */ - private static class StubExoPlayer implements ExoPlayer, Handler.Callback { + private static class MessageHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { private final Handler handler; - public StubExoPlayer() { + public MessageHandlingExoPlayer() { HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); handlerThread.start(); handler = new Handler(handlerThread.getLooper(), this); } - @Override - public Looper getPlaybackLooper() { - throw new UnsupportedOperationException(); - } - - @Override - public void addListener(Player.EventListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeListener(Player.EventListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public int getPlaybackState() { - throw new UnsupportedOperationException(); - } - - @Override - public void prepare(MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getPlayWhenReady() { - throw new UnsupportedOperationException(); - } - - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - throw new UnsupportedOperationException(); - } - - @Override - public int getRepeatMode() { - throw new UnsupportedOperationException(); - } - - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getShuffleModeEnabled() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isLoading() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - throw new UnsupportedOperationException(); - } - - @Override - public PlaybackParameters getPlaybackParameters() { - throw new UnsupportedOperationException(); - } - - @Override - public void stop() { - throw new UnsupportedOperationException(); - } - - @Override - public void release() { - throw new UnsupportedOperationException(); - } - @Override public void sendMessages(ExoPlayerMessage... messages) { handler.obtainMessage(0, messages).sendToTarget(); } - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - public int getRendererCount() { - throw new UnsupportedOperationException(); - } - - @Override - public int getRendererType(int index) { - throw new UnsupportedOperationException(); - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - throw new UnsupportedOperationException(); - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - throw new UnsupportedOperationException(); - } - - @Override - public Object getCurrentManifest() { - throw new UnsupportedOperationException(); - } - - @Override - public Timeline getCurrentTimeline() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentPeriodIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getNextWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getPreviousWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public long getDuration() { - throw new UnsupportedOperationException(); - } - - @Override - public long getCurrentPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public long getBufferedPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public int getBufferedPercentage() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCurrentWindowDynamic() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCurrentWindowSeekable() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isPlayingAd() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentAdGroupIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentAdIndexInAdGroup() { - throw new UnsupportedOperationException(); - } - - @Override - public long getContentPosition() { - throw new UnsupportedOperationException(); - } - @Override public boolean handleMessage(Message msg) { ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; @@ -919,6 +697,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } return true; } + } } 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 4d53a6c89d..4a5beb0501 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 @@ -69,7 +69,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return player; } - private static class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, + private static class FakeExoPlayer extends StubExoPlayer implements MediaSource.Listener, MediaPeriod.Callback, Runnable { private final Renderer[] renderers; @@ -144,21 +144,11 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return true; } - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - throw new UnsupportedOperationException(); - } - @Override public int getRepeatMode() { return Player.REPEAT_MODE_OFF; } - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new UnsupportedOperationException(); - } - @Override public boolean getShuffleModeEnabled() { return false; @@ -169,31 +159,6 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return isLoading; } - @Override - public void seekToDefaultPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - throw new UnsupportedOperationException(); - } - @Override public PlaybackParameters getPlaybackParameters() { return PlaybackParameters.DEFAULT; @@ -357,16 +322,6 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { }); } - @Override - public void sendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - // MediaSource.Listener @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java index 88b5de7f65..7cae709438 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; - /** * Provides ogg/vorbis test data in bytes for unit tests. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java new file mode 100644 index 0000000000..e03f6fbad9 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.testutil; + +import android.os.Looper; +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.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; + +/** + * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} + * from every method. + */ +public abstract class StubExoPlayer implements ExoPlayer { + + @Override + public Looper getPlaybackLooper() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeListener(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public int getPlaybackState() { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getPlayWhenReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getShuffleModeEnabled() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLoading() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererCount() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererType(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + throw new UnsupportedOperationException(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getCurrentManifest() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getNextWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCurrentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public int getBufferedPercentage() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowSeekable() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayingAd() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdGroupIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentPosition() { + throw new UnsupportedOperationException(); + } + +} From 57868092eaec2cb02ba6bd74407d0621ec08e734 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 15 Nov 2017 03:08:11 -0800 Subject: [PATCH 055/148] Add Builder pattern to HlsMediaSource. Add Builder pattern to HlsMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175803853 --- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/source/hls/HlsMediaSource.java | 136 +++++++++++++++++- 2 files changed, 138 insertions(+), 3 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 65e1c0e083..3d669c9477 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 @@ -374,7 +374,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_HLS: - return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); + return HlsMediaSource.Builder + .forDataSource(uri, mediaDataSourceFactory) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_OTHER: return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, eventLogger); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 21b27e655d..3f28981f0e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -47,6 +47,132 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } + /** + * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri manifestUri; + private final HlsDataSourceFactory hlsDataSourceFactory; + + private HlsExtractorFactory extractorFactory; + private ParsingLoadable.Parser playlistParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + private int minLoadableRetryCount; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and + * a {@link DataSource.Factory}. + * + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory A data source factory that will be wrapped by a + * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, + * segments and keys. + * @return A new builder. + */ + public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { + return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + } + + /** + * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and + * a {@link HlsDataSourceFactory}. + * + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * manifests, segments and keys. + * @return A new builder. + */ + public static Builder forHlsDataSource(Uri manifestUri, + HlsDataSourceFactory dataSourceFactory) { + return new Builder(manifestUri, dataSourceFactory); + } + + private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { + this.manifestUri = manifestUri; + this.hlsDataSourceFactory = hlsDataSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } + + /** + * Sets the factory for {@link Extractor}s for the segments. Default value is + * {@link HlsExtractorFactory#DEFAULT}. + * + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the + * segments. + * @return This builder. + */ + public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { + this.extractorFactory = extractorFactory; + return this; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times loads must be retried before + * errors are propagated. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the parser to parse HLS playlists. The default is an instance of + * {@link HlsPlaylistParser}. + * + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @return This builder. + */ + public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { + this.playlistParser = playlistParser; + return this; + } + + /** + * Builds a new {@link HlsMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link HlsMediaSource}. + */ + public HlsMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + if (extractorFactory == null) { + extractorFactory = HlsExtractorFactory.DEFAULT; + } + if (playlistParser == null) { + playlistParser = new HlsPlaylistParser(); + } + return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, + minLoadableRetryCount, eventHandler, eventListener, playlistParser); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -69,7 +195,9 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, @@ -85,7 +213,9 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -105,10 +235,12 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; From d49fc54968b33c027ca205597a81c1f36cf110ec Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Nov 2017 09:46:28 -0800 Subject: [PATCH 056/148] Use ArrayDeque for playback parameters checkpoints ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175837754 --- .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 73c0bc20be..2180601481 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -36,8 +36,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.LinkedList; /** * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback @@ -174,7 +174,7 @@ public final class DefaultAudioSink implements AudioSink { private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; - private final LinkedList playbackParametersCheckpoints; + private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; /** @@ -277,7 +277,7 @@ public final class DefaultAudioSink implements AudioSink { drainingAudioProcessorIndex = C.INDEX_UNSET; this.audioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; - playbackParametersCheckpoints = new LinkedList<>(); + playbackParametersCheckpoints = new ArrayDeque<>(); } @Override From 108900cf5292211e2dc54c1b22ba1fd46cf63ea0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 01:07:51 -0800 Subject: [PATCH 057/148] Add support for float output in DefaultAudioSink Also switch from using MIME types to C.ENCODING_* encodings in DefaultAudioSink. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175936872 --- RELEASENOTES.md | 4 + .../java/com/google/android/exoplayer2/C.java | 10 +- .../android/exoplayer2/audio/AudioSink.java | 30 ++-- .../exoplayer2/audio/DefaultAudioSink.java | 134 ++++++++---------- .../audio/MediaCodecAudioRenderer.java | 20 ++- .../audio/SimpleDecoderAudioRenderer.java | 4 +- .../android/exoplayer2/util/MimeTypes.java | 30 +++- .../google/android/exoplayer2/util/Util.java | 1 + 8 files changed, 124 insertions(+), 109 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 579c2a92ac..cca26f8063 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Release notes # +### dev-v2 (not yet released) ### + +* Support 32-bit PCM float output from `DefaultAudioSink`. + ### 2.6.0 ### * Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 9d4049ada9..592589e221 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -127,8 +127,8 @@ public final class C { */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS, - ENCODING_DTS_HD}) + ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_DTS, ENCODING_DTS_HD}) public @interface Encoding {} /** @@ -136,7 +136,7 @@ public final class C { */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT}) + ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) public @interface PcmEncoding {} /** * @see AudioFormat#ENCODING_INVALID @@ -158,6 +158,10 @@ public final class C { * PCM encoding with 32 bits per sample. */ public static final int ENCODING_PCM_32BIT = 0x40000000; + /** + * @see AudioFormat#ENCODING_PCM_FLOAT + */ + public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; /** * @see AudioFormat#ENCODING_AC3 */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 5408032907..faf3160018 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -25,14 +25,13 @@ import java.nio.ByteBuffer; * A sink that consumes audio data. *

      * Before starting playback, specify the input audio format by calling - * {@link #configure(String, int, int, int, int, int[], int, int)}. + * {@link #configure(int, int, int, int, int[], int, int)}. *

      * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

      - * Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format - * changes. The sink will be reinitialized on the next call to - * {@link #handleBuffer(ByteBuffer, long)}. + * Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format changes. + * The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}. *

      * Call {@link #reset()} to prepare the sink to receive audio data from a new playback position. *

      @@ -166,13 +165,12 @@ public interface AudioSink { void setListener(Listener listener); /** - * Returns whether it's possible to play audio in the specified format using encoded audio - * passthrough. + * Returns whether it's possible to play audio in the specified encoding using passthrough. * - * @param mimeType The format mime type. - * @return Whether it's possible to play audio in the format using encoded audio passthrough. + * @param encoding The audio encoding. + * @return Whether it's possible to play audio in the specified encoding using passthrough. */ - boolean isPassthroughSupported(String mimeType); + boolean isPassthroughSupported(@C.Encoding int encoding); /** * Returns the playback position in the stream starting at zero, in microseconds, or @@ -186,12 +184,9 @@ public interface AudioSink { /** * Configures (or reconfigures) the sink. * - * @param inputMimeType The MIME type of audio data provided in the input buffers. + * @param inputEncoding The encoding of audio data provided in the input buffers. * @param inputChannelCount The number of channels. * @param inputSampleRate The sample rate in Hz. - * @param inputPcmEncoding For PCM formats, the encoding used. One of - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} - * and {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size. * @param outputChannels A mapping from input to output channels that is applied to this sink's @@ -205,9 +200,9 @@ public interface AudioSink { * immediately preceding the next call to {@link #reset()} or this method. * @throws ConfigurationException If an error occurs configuring the sink. */ - void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, - @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, - int trimStartSamples, int trimEndSamples) throws ConfigurationException; + void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException; /** * Starts or resumes consuming audio if initialized. @@ -228,8 +223,7 @@ public interface AudioSink { * Returns whether the data was handled in full. If the data was not handled in full then the same * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, * except in the case of an intervening call to {@link #reset()} (or to - * {@link #configure(String, int, int, int, int, int[], int, int)} that causes the sink to be - * reset). + * {@link #configure(int, int, int, int, int[], int, int)} that causes the sink to be reset). * * @param buffer The buffer containing audio data. * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 2180601481..0d3365b5d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -29,7 +29,6 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; 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; @@ -182,13 +181,13 @@ public final class DefaultAudioSink implements AudioSink { */ private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; + private boolean isInputPcm; private int inputSampleRate; private int sampleRate; private int channelConfig; - private @C.Encoding int encoding; private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; - private boolean passthrough; + private boolean processingEnabled; private int bufferSize; private long bufferSizeUs; @@ -286,9 +285,8 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public boolean isPassthroughSupported(String mimeType) { - return audioCapabilities != null - && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); + public boolean isPassthroughSupported(@C.Encoding int encoding) { + return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); } @Override @@ -331,18 +329,20 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, - @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, - int trimStartSamples, int trimEndSamples) throws ConfigurationException { + public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException { + boolean flush = false; this.inputSampleRate = inputSampleRate; int channelCount = inputChannelCount; int sampleRate = inputSampleRate; - @C.Encoding int encoding; - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType); - boolean flush = false; - if (!passthrough) { - encoding = inputPcmEncoding; - pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount); + isInputPcm = isEncodingPcm(inputEncoding); + if (isInputPcm) { + pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); + } + @C.Encoding int encoding = inputEncoding; + boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + if (processingEnabled) { trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { @@ -360,8 +360,6 @@ public final class DefaultAudioSink implements AudioSink { if (flush) { resetAudioProcessors(); } - } else { - encoding = getEncodingForMimeType(inputMimeType); } int channelConfig; @@ -411,11 +409,11 @@ public final class DefaultAudioSink implements AudioSink { // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) - if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { + if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate + if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -423,16 +421,24 @@ public final class DefaultAudioSink implements AudioSink { reset(); - this.encoding = encoding; - this.passthrough = passthrough; + this.processingEnabled = processingEnabled; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; - outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); - + outputEncoding = encoding; + if (isInputPcm) { + outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount); + } if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; - } else if (passthrough) { + } else if (isInputPcm) { + int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); + Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); + int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; + int maxAppBufferSize = (int) Math.max(minBufferSize, + durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); + bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); + } else { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { @@ -442,21 +448,9 @@ public final class DefaultAudioSink implements AudioSink { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } - } else { - int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); - Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); - int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; - int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); - bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize - : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize - : multipliedBufferSize; } - bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize); - - // The old playback parameters may no longer be applicable so try to reset them now. - setPlaybackParameters(playbackParameters); + bufferSizeUs = + isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; } private void resetAudioProcessors() { @@ -487,6 +481,10 @@ public final class DefaultAudioSink implements AudioSink { releasingConditionVariable.block(); audioTrack = initializeAudioTrack(); + + // The old playback parameters may no longer be applicable so try to reset them now. + setPlaybackParameters(playbackParameters); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { @@ -574,7 +572,7 @@ public final class DefaultAudioSink implements AudioSink { return true; } - if (passthrough && framesPerEncodedSample == 0) { + if (!isInputPcm && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } @@ -618,20 +616,19 @@ public final class DefaultAudioSink implements AudioSink { } } - if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; - } else { + if (isInputPcm) { submittedPcmBytes += buffer.remaining(); + } else { + submittedEncodedFrames += framesPerEncodedSample; } inputBuffer = buffer; } - if (passthrough) { - // Passthrough buffers are not processed. - writeBuffer(inputBuffer, presentationTimeUs); - } else { + if (processingEnabled) { processBuffers(presentationTimeUs); + } else { + writeBuffer(inputBuffer, presentationTimeUs); } if (!inputBuffer.hasRemaining()) { @@ -679,10 +676,9 @@ public final class DefaultAudioSink implements AudioSink { } @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) - throws WriteException { + private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException { if (!buffer.hasRemaining()) { - return true; + return; } if (outputBuffer != null) { Assertions.checkArgument(outputBuffer == buffer); @@ -701,7 +697,7 @@ public final class DefaultAudioSink implements AudioSink { } int bytesRemaining = buffer.remaining(); int bytesWritten = 0; - if (Util.SDK_INT < 21) { // passthrough == false + if (Util.SDK_INT < 21) { // isInputPcm == true // Work out how many bytes we can write without the risk of blocking. int bytesPending = (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); @@ -728,17 +724,15 @@ public final class DefaultAudioSink implements AudioSink { throw new WriteException(bytesWritten); } - if (!passthrough) { + if (isInputPcm) { writtenPcmBytes += bytesWritten; } if (bytesWritten == bytesRemaining) { - if (passthrough) { + if (!isInputPcm) { writtenEncodedFrames += framesPerEncodedSample; } outputBuffer = null; - return true; } - return false; } @Override @@ -758,7 +752,7 @@ public final class DefaultAudioSink implements AudioSink { private boolean drainAudioProcessorsToEndOfStream() throws WriteException { boolean audioProcessorNeedsEndOfStream = false; if (drainingAudioProcessorIndex == C.INDEX_UNSET) { - drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; + drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length; audioProcessorNeedsEndOfStream = true; } while (drainingAudioProcessorIndex < audioProcessors.length) { @@ -799,8 +793,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (passthrough) { - // The playback parameters are always the default in passthrough mode. + if (isInitialized() && !processingEnabled) { + // The playback parameters are always the default if processing is disabled. this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; } @@ -1076,7 +1070,7 @@ public final class DefaultAudioSink implements AudioSink { audioTimestampSet = false; } } - if (getLatencyMethod != null && !passthrough) { + if (getLatencyMethod != null && isInputPcm) { try { // Compute the audio track latency, excluding the latency due to the buffer (leaving // latency due to the mixer and audio hardware driver). @@ -1115,11 +1109,11 @@ public final class DefaultAudioSink implements AudioSink { } private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); + return isInputPcm ? (submittedPcmBytes / pcmFrameSize) : submittedEncodedFrames; } private long getWrittenFrames() { - return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); + return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames; } private void resetSyncParams() { @@ -1212,20 +1206,10 @@ public final class DefaultAudioSink implements AudioSink { MODE_STATIC, audioSessionId); } - @C.Encoding - private static int getEncodingForMimeType(String mimeType) { - switch (mimeType) { - case MimeTypes.AUDIO_AC3: - return C.ENCODING_AC3; - case MimeTypes.AUDIO_E_AC3: - return C.ENCODING_E_AC3; - case MimeTypes.AUDIO_DTS: - return C.ENCODING_DTS; - case MimeTypes.AUDIO_DTS_HD: - return C.ENCODING_DTS_HD; - default: - return C.ENCODING_INVALID; - } + private static boolean isEncodingPcm(@C.Encoding int encoding) { + return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; } private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8206e94cf..18cbcea115 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 @@ -51,6 +51,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; + @C.Encoding private int pcmEncoding; private int channelCount; private int encoderDelay; @@ -226,7 +227,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioSink.isPassthroughSupported(mimeType); + return audioSink.isPassthroughSupported(MimeTypes.getEncoding(mimeType)); } @Override @@ -272,10 +273,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) throws ExoPlaybackException { - boolean passthrough = passthroughMediaFormat != null; - String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) - : MimeTypes.AUDIO_RAW; - MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; + @C.Encoding int encoding; + MediaFormat format; + if (passthroughMediaFormat != null) { + encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME)); + format = passthroughMediaFormat; + } else { + encoding = pcmEncoding; + format = outputFormat; + } int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; @@ -289,8 +295,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, - encoderDelay, encoderPadding); + audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, + encoderPadding); } catch (AudioSink.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } 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 98a84fdff8..6be4b1d35d 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 @@ -329,8 +329,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); - audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount, - outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding); + audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount, + outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding); audioTrackNeedsConfigure = false; } 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 2daf16d3d2..d48d28caa5 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 @@ -208,12 +208,12 @@ public final class MimeTypes { } /** - * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. - * {@link C#TRACK_TYPE_UNKNOWN} if the mime type is not known or the mapping cannot be + * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. + * {@link C#TRACK_TYPE_UNKNOWN} if the MIME type is not known or the mapping cannot be * established. * - * @param mimeType The mimeType. - * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. + * @param mimeType The MIME type. + * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. */ public static int getTrackType(String mimeType) { if (TextUtils.isEmpty(mimeType)) { @@ -239,6 +239,28 @@ public final class MimeTypes { } } + /** + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID} if the mapping cannot be established. + * + * @param mimeType The MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type. + */ + public static @C.Encoding int getEncoding(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_AC3: + return C.ENCODING_AC3; + case MimeTypes.AUDIO_E_AC3: + return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_DTS: + return C.ENCODING_DTS; + case MimeTypes.AUDIO_DTS_HD: + return C.ENCODING_DTS_HD; + default: + return C.ENCODING_INVALID; + } + } + /** * Equivalent to {@code getTrackType(getMediaMimeType(codec))}. * 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 a79ed38755..5b2de1042e 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 @@ -801,6 +801,7 @@ public final class Util { case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: return channelCount * 4; case C.ENCODING_INVALID: case Format.NO_VALUE: From 820a4459448176a9b06a64983127d92a52b4e0b3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 02:03:02 -0800 Subject: [PATCH 058/148] Add support for float output for FfmpegAudioRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175940553 --- RELEASENOTES.md | 3 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 52 +++++++++++++++++-- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 29 ++++++++--- extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc | 27 ++++++---- .../ext/flac/LibflacAudioRenderer.java | 3 ++ .../ext/opus/LibopusAudioRenderer.java | 2 + .../android/exoplayer2/audio/AudioSink.java | 8 +-- .../exoplayer2/audio/DefaultAudioSink.java | 11 +++- .../audio/MediaCodecAudioRenderer.java | 12 +++-- .../audio/SimpleDecoderAudioRenderer.java | 10 ++++ .../android/exoplayer2/util/MimeTypes.java | 7 +-- 11 files changed, 130 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cca26f8063..d2d0105b55 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,8 @@ ### dev-v2 (not yet released) ### -* Support 32-bit PCM float output from `DefaultAudioSink`. +* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to + use this with `FfmpegAudioRenderer`. ### 2.6.0 ### 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 ed8a5b0eac..3e23659bf8 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 @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; 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.AudioSink; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -41,6 +43,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + private final boolean enableFloatOutput; + private FfmpegDecoder decoder; public FfmpegAudioRenderer() { @@ -55,7 +59,23 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { - super(eventHandler, eventListener, audioProcessors); + this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the + * device/build and if the input format may have bit depth higher than 16-bit. When using + * 32-bit float output, any audio processing will be disabled, including playback speed/pitch + * adjustment. + */ + public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioSink audioSink, boolean enableFloatOutput) { + super(eventHandler, eventListener, null, false, audioSink); + this.enableFloatOutput = enableFloatOutput; } @Override @@ -64,7 +84,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { String sampleMimeType = format.sampleMimeType; if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) { + } else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -82,7 +102,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - format.sampleMimeType, format.initializationData); + format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format)); return decoder; } @@ -90,8 +110,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { public Format getOutputFormat() { int channelCount = decoder.getChannelCount(); int sampleRate = decoder.getSampleRate(); + @C.PcmEncoding int encoding = decoder.getEncoding(); return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null); + Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null); + } + + private boolean isOutputSupported(Format inputFormat) { + return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT); + } + + private boolean shouldUseFloatOutput(Format inputFormat) { + if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) { + return false; + } + switch (inputFormat.sampleMimeType) { + case MimeTypes.AUDIO_RAW: + // For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit. + return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT + || inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT + || inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT; + case MimeTypes.AUDIO_AC3: + // AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding. + return false; + default: + // For all other formats, assume that it's worth using 32-bit float encoding. + return true; + } } } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 2af2101ee7..8807738cfa 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; @@ -29,11 +30,15 @@ import java.util.List; /* package */ final class FfmpegDecoder extends SimpleDecoder { - // Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio. - private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2; + // Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2; + // Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; private final String codecName; private final byte[] extraData; + private final @C.Encoding int encoding; + private final int outputBufferSize; private long nativeContext; // May be reassigned on resetting the codec. private boolean hasOutputFormat; @@ -41,14 +46,17 @@ import java.util.List; private volatile int sampleRate; public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - String mimeType, List initializationData) throws FfmpegDecoderException { + String mimeType, List initializationData, boolean outputFloat) + throws FfmpegDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (!FfmpegLibrary.isAvailable()) { throw new FfmpegDecoderException("Failed to load decoder native libraries."); } codecName = FfmpegLibrary.getCodecName(mimeType); extraData = getExtraData(mimeType, initializationData); - nativeContext = ffmpegInitialize(codecName, extraData); + encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; + outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; + nativeContext = ffmpegInitialize(codecName, extraData, outputFloat); if (nativeContext == 0) { throw new FfmpegDecoderException("Initialization failed."); } @@ -81,8 +89,8 @@ import java.util.List; } ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); - ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE); - int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE); + ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); + int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); if (result < 0) { return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); } @@ -124,6 +132,13 @@ import java.util.List; return sampleRate; } + /** + * Returns the encoding of output audio. + */ + public @C.Encoding int getEncoding() { + return encoding; + } + /** * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if * not required. @@ -153,7 +168,7 @@ import java.util.List; } } - private native long ffmpegInitialize(String codecName, byte[] extraData); + private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat); private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize); private native int ffmpegGetChannelCount(long context); diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index fa615f2ec1..d077c819ab 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -57,8 +57,10 @@ extern "C" { #define ERROR_STRING_BUFFER_LENGTH 256 -// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT. -static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16; +// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT. +static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16; +// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT. +static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT; /** * Returns the AVCodec with the specified name, or NULL if it is not available. @@ -71,7 +73,7 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName); * Returns the created context. */ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData); + jbyteArray extraData, jboolean outputFloat); /** * Decodes the packet into the output buffer, returning the number of bytes @@ -107,13 +109,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { return getCodecByName(env, codecName) != NULL; } -DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData) { +DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, + jboolean outputFloat) { AVCodec *codec = getCodecByName(env, codecName); if (!codec) { LOGE("Codec not found."); return 0L; } - return (jlong) createContext(env, codec, extraData); + return (jlong) createContext(env, codec, extraData, outputFloat); } DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, @@ -177,7 +180,8 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { LOGE("Unexpected error finding codec %d.", codecId); return 0L; } - return (jlong) createContext(env, codec, extraData); + return (jlong) createContext(env, codec, extraData, + context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT); } avcodec_flush_buffers(context); @@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) { } AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData) { + jbyteArray extraData, jboolean outputFloat) { AVCodecContext *context = avcodec_alloc_context3(codec); if (!context) { LOGE("Failed to allocate context."); return NULL; } - context->request_sample_fmt = OUTPUT_FORMAT; + context->request_sample_fmt = + outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT; if (extraData) { jsize size = env->GetArrayLength(extraData); context->extradata_size = size; @@ -275,7 +280,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0); - av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0); + // The output format is always the requested format. + av_opt_set_int(resampleContext, "out_sample_fmt", + context->request_sample_fmt, 0); result = avresample_open(resampleContext); if (result < 0) { logError("avresample_open", result); @@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, context->opaque = resampleContext; } int inSampleSize = av_get_bytes_per_sample(sampleFormat); - int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT); + int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt); int outSamples = avresample_get_out_samples(resampleContext, sampleCount); int bufferOutSize = outSampleSize * channelCount * outSamples; if (outSize + bufferOutSize > outputSize) { 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 dc376d2ea4..a72b03cd44 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -52,6 +53,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { if (!FlacLibrary.isAvailable() || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { + return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } else { 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 e4745d0c29..b94f3e9332 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 @@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { if (!OpusLibrary.isAvailable() || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { + return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index faf3160018..6bb5bf7d8e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -75,7 +75,7 @@ public interface AudioSink { * * @param bufferSize The size of the sink's buffer, in bytes. * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for - * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the * buffered media can have a variable bitrate so the duration may be unknown. * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds. */ @@ -165,12 +165,12 @@ public interface AudioSink { void setListener(Listener listener); /** - * Returns whether it's possible to play audio in the specified encoding using passthrough. + * Returns whether it's possible to play audio in the specified encoding. * * @param encoding The audio encoding. - * @return Whether it's possible to play audio in the specified encoding using passthrough. + * @return Whether it's possible to play audio in the specified encoding. */ - boolean isPassthroughSupported(@C.Encoding int encoding); + boolean isEncodingSupported(@C.Encoding int encoding); /** * Returns the playback position in the stream starting at zero, in microseconds, or diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 0d3365b5d8..ba62ac126e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -285,8 +285,15 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public boolean isPassthroughSupported(@C.Encoding int encoding) { - return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); + public boolean isEncodingSupported(@C.Encoding int encoding) { + if (isEncodingPcm(encoding)) { + // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float + // output from platform API version 21 only. Other integer PCM encodings are resampled by this + // sink to 16-bit PCM. + return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; + } else { + return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 18cbcea115..25ad847f7e 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 @@ -178,6 +178,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media && mediaCodecSelector.getPassthroughDecoderInfo() != null) { return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; } + if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding)) + || !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) { + // Assume the decoder outputs 16-bit PCM, unless the input is raw. + return FORMAT_UNSUPPORTED_SUBTYPE; + } boolean requiresSecureDecryption = false; DrmInitData drmInitData = format.drmInitData; if (drmInitData != null) { @@ -220,14 +225,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /** * Returns whether encoded audio passthrough should be used for playing back the input format. - * This implementation returns true if the {@link AudioSink} indicates that passthrough is - * supported. + * This implementation returns true if the {@link AudioSink} indicates that encoded audio output + * is supported. * * @param mimeType The type of input media. * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioSink.isPassthroughSupported(MimeTypes.getEncoding(mimeType)); + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 6be4b1d35d..d9ad549104 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 @@ -200,6 +200,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager, Format format); + /** + * Returns whether the audio sink can accept audio in the specified encoding. + * + * @param encoding The audio encoding. + * @return Whether the audio sink can accept audio in the specified encoding. + */ + protected final boolean supportsOutputEncoding(@C.Encoding int encoding) { + return audioSink.isEncodingSupported(encoding); + } + @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { 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 d48d28caa5..c29a4c3717 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 @@ -240,11 +240,12 @@ public final class MimeTypes { } /** - * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or - * {@link C#ENCODING_INVALID} if the mapping cannot be established. + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to specified MIME type, if + * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise. * * @param mimeType The MIME type. - * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID}. */ public static @C.Encoding int getEncoding(String mimeType) { switch (mimeType) { From f5a3a277263aaa05057ac06100ad8d997ce7fbf2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Nov 2017 05:08:44 -0800 Subject: [PATCH 059/148] Deduplicate ExtractorMediaPeriod discontinuity reporting This change makes sure progress is being made before reporting a discontinuity. Else in cases like having no network and playing a live stream, we allow the discontinuity to be read each time an internal retry occurs, meaning it gets read repeatedly. This does no harm, but is noisy and unnecessary. We should also not allow skipping whilst there is a pending reset or discontinuity notification, just like we don't allow reads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175953064 --- .../google/android/exoplayer2/source/ExtractorMediaPeriod.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 1228061cde..d43b2d87b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -303,7 +303,8 @@ import java.util.Arrays; @Override public long readDiscontinuity() { - if (notifyDiscontinuity) { + if (notifyDiscontinuity + && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) { notifyDiscontinuity = false; return lastSeekPositionUs; } From 1f70d3cdd7b8487c8754f303932cd16a9e7c2e42 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 17 Nov 2017 03:08:55 -0800 Subject: [PATCH 060/148] Add Builder to ExtractorMediaSource. Add Builder pattern to ExtractorMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176088810 --- RELEASENOTES.md | 2 + .../exoplayer2/imademo/PlayerManager.java | 9 +- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 123 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 8 +- 8 files changed, 146 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d2d0105b55..2a5ccb583c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, + DashMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index e11c840d12..6b840830c5 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -21,8 +21,6 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -69,13 +67,10 @@ import com.google.android.exoplayer2.util.Util; DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getString(R.string.application_name))); - // Produces Extractor instances for parsing the content media (i.e. not the ad). - ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = new ExtractorMediaSource( - Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); + MediaSource contentMediaSource = + new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, 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 3d669c9477..ca253db809 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 @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.source.BehindLiveWindowException; @@ -379,8 +378,9 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_OTHER: - return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), - mainHandler, eventLogger); + return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) + .setEventListener(mainHandler, eventLogger) + .build(); default: { throw new IllegalStateException("Unsupported type: " + type); } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 65fb4c8195..bd6e698dc6 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -76,12 +76,10 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 591f43f38a..aa61df74d9 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -76,12 +76,10 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index c2c1867a90..746f3d273f 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -105,12 +105,10 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 1b3f6cb95c..066953b998 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -98,6 +98,123 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; + /** + * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + + private ExtractorsFactory extractorsFactory; + private int minLoadableRetryCount; + private Handler eventHandler; + private EventListener eventListener; + private String customCacheKey; + private int continueLoadingCheckIntervalBytes; + private boolean isBuildCalled; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + */ + public Builder(Uri uri, DataSource.Factory dataSourceFactory) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + + minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; + continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the factory for {@link Extractor}s to process the media stream. Default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This builder. + */ + public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * Default value is null. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This builder. + */ + public Builder setCustomCacheKey(String customCacheKey) { + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets the number of bytes that should be loaded between each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value + * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This builder. + */ + public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Builds a new {@link ExtractorMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link ExtractorMediaSource}. + */ + public ExtractorMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + if (extractorsFactory == null) { + extractorsFactory = new DefaultExtractorsFactory(); + } + return new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, + minLoadableRetryCount, eventHandler, eventListener, customCacheKey, + continueLoadingCheckIntervalBytes); + } + + } + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. @@ -106,7 +223,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. 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. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); @@ -122,7 +241,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, String customCacheKey) { @@ -143,7 +264,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 18aa8a63e7..397b8effd3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -23,7 +23,6 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -173,9 +172,10 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, - new DefaultExtractorsFactory(), mainHandler, componentListener); + MediaSource adMediaSource = new ExtractorMediaSource.Builder( + adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) + .setEventListener(mainHandler, componentListener) + .build(); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; From bb4be4314a942998e895114e471bd38c3e2ce62e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 17 Nov 2017 05:26:58 -0800 Subject: [PATCH 061/148] Add simplified FakeTimeline constructor. This is helpful for tests which don't care about detailled timeline set-ups besides the number of windows. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176097369 --- .../android/exoplayer2/ExoPlayerTest.java | 42 +++------- .../testutil/ExoPlayerTestRunner.java | 8 +- .../exoplayer2/testutil/FakeTimeline.java | 76 ++++++++++++++++++- 3 files changed, 89 insertions(+), 37 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 0edd19bc09..95d5d96163 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 @@ -27,7 +27,6 @@ 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 com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; @@ -68,7 +67,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Object manifest = new Object(); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -87,10 +86,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 0), - new TimelineWindowDefinition(false, false, 0), - new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) @@ -107,10 +103,7 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 10), - new TimelineWindowDefinition(false, false, 10), - new TimelineWindowDefinition(false, false, 10)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { @@ -151,7 +144,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepreparationGivesFreshSourceInfo() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest, @@ -218,10 +211,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepeatModeChanges() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1 .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1 @@ -241,7 +231,7 @@ public final class ExoPlayerTest extends TestCase { } public void testShuffleModeEnabledChanges() throws Exception { - Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), @@ -264,7 +254,6 @@ public final class ExoPlayerTest extends TestCase { } public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { - Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased") .setRepeatMode(Player.REPEAT_MODE_ALL) @@ -274,15 +263,13 @@ public final class ExoPlayerTest extends TestCase { .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish. .build(); new ExoPlayerTestRunner.Builder() - .setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule) + .setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); assertTrue(renderer.isEnded); } public void testSeekProcessedCallback() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") // Initial seek before timeline preparation finished. .pause().seek(10).waitForPlaybackState(Player.STATE_READY) @@ -314,8 +301,7 @@ public final class ExoPlayerTest extends TestCase { } public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -343,9 +329,7 @@ public final class ExoPlayerTest extends TestCase { } public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), - new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -374,8 +358,7 @@ public final class ExoPlayerTest extends TestCase { public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -414,8 +397,7 @@ public final class ExoPlayerTest extends TestCase { public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); 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 a1f8fc7861..591e63dc5b 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 @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; @@ -103,8 +102,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The - * default value is a non-seekable, non-dynamic {@link FakeTimeline} with zero duration. Setting - * the timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of + * {@link FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the + * timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. @@ -294,7 +294,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } if (mediaSource == null) { if (timeline == null) { - timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + timeline = new FakeTimeline(1); } mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 2937ee2770..4a9d79f906 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -30,7 +30,10 @@ public final class FakeTimeline extends Timeline { */ public static final class TimelineWindowDefinition { - private static final int WINDOW_DURATION_US = 100000; + /** + * Default test window duration in microseconds. + */ + public static final int DEFAULT_WINDOW_DURATION_US = 100_000; public final int periodCount; public final Object id; @@ -40,19 +43,65 @@ public final class FakeTimeline extends Timeline { public final int adGroupsPerPeriodCount; public final int adsPerAdGroupCount; - public TimelineWindowDefinition(int periodCount, Object id) { - this(periodCount, id, true, false, WINDOW_DURATION_US); + /** + * Creates a seekable, non-dynamic window definition with one period with a duration of + * {@link #DEFAULT_WINDOW_DURATION_US}. + */ + public TimelineWindowDefinition() { + this(1, 0, true, false, DEFAULT_WINDOW_DURATION_US); } + /** + * Creates a seekable, non-dynamic window definition with a duration of + * {@link #DEFAULT_WINDOW_DURATION_US}. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + */ + public TimelineWindowDefinition(int periodCount, Object id) { + this(periodCount, id, true, false, DEFAULT_WINDOW_DURATION_US); + } + + /** + * Creates a window definition with one period. + * + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { this(1, 0, isSeekable, isDynamic, durationUs); } + /** + * Creates a window definition. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) { this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); } + /** + * Creates a window definition with ad groups. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + * @param adGroupsCountPerPeriod The number of ad groups in each period. The position of the ad + * groups is equally distributed in each period starting. + * @param adsPerAdGroupCount The number of ads in each ad group. + */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { this.periodCount = periodCount; @@ -71,6 +120,21 @@ public final class FakeTimeline extends Timeline { private final TimelineWindowDefinition[] windowDefinitions; private final int[] periodOffsets; + /** + * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period + * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. + * + * @param windowCount The number of windows. + */ + public FakeTimeline(int windowCount) { + this(createDefaultWindowDefinitions(windowCount)); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { this.windowDefinitions = windowDefinitions; periodOffsets = new int[windowDefinitions.length + 1]; @@ -141,4 +205,10 @@ public final class FakeTimeline extends Timeline { return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; } + private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { + TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; + Arrays.fill(windowDefinitions, new TimelineWindowDefinition()); + return windowDefinitions; + } + } From 14299e0643cca287e3c4920c258ef9340e7ba38f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:18:06 -0800 Subject: [PATCH 062/148] Simplify LoopingMediaSourceTest setup This test seems to obtain a timeline from a prepared FakeMediaSource, but that's identical to the timeline passed into that source to start with :). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176117133 --- .../exoplayer2/source/LoopingMediaSourceTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 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 2c8deb74b4..79f646b5c4 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 @@ -30,12 +30,13 @@ import junit.framework.TestCase; */ public class LoopingMediaSourceTest extends TestCase { - private final Timeline multiWindowTimeline; + private FakeTimeline multiWindowTimeline; - public LoopingMediaSourceTest() { - multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource( - new FakeTimeline(new TimelineWindowDefinition(1, 111), - new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null)); + @Override + public void setUp() throws Exception { + super.setUp(); + multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111), + new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)); } public void testSingleLoop() { From 1b7c950d1e6639e2a2f10140598d6646c8257122 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:22:17 -0800 Subject: [PATCH 063/148] Add MediaSourceTestRunner for MediaSource tests. - MediaSourceTestRunner aims to encapsulate some of the logic currently used in DynamicConcatenatingMediaSourceTest, so it can be re-used for testing other MediaSource implementations. - The change also fixes DynamicConcatenatingMediaSourceTest to execute calls on the correct threads, and to release handler threads at the end of each test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176117535 --- .../source/ConcatenatingMediaSourceTest.java | 29 +- .../DynamicConcatenatingMediaSourceTest.java | 561 ++++++++---------- .../testutil/MediaSourceTestRunner.java | 290 +++++++++ .../android/exoplayer2/testutil/TestUtil.java | 1 + .../exoplayer2/testutil/TimelineAsserts.java | 50 -- 5 files changed, 572 insertions(+), 359 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java 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 6f6556225e..429325defc 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 @@ -23,6 +23,7 @@ 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.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -32,6 +33,8 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { + private static final int TIMEOUT_MS = 10000; + public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -208,18 +211,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - // Prepare and assert timeline contains ad groups. - Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource); - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + try { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); - // Create all periods and assert period creation of child media sources has been called. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + // Create all periods and assert period creation of child media sources has been called. + testRunner.assertPrepareAndReleaseAllPeriods(); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } finally { + testRunner.release(); + } } /** 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 96d11678c9..536180fafc 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 @@ -20,19 +20,15 @@ import static org.mockito.Mockito.verify; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import android.os.Message; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.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.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.StubExoPlayer; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.util.Arrays; import junit.framework.TestCase; @@ -45,78 +41,84 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private static final int TIMEOUT_MS = 10000; - private Timeline timeline; - private boolean timelineUpdated; - private boolean customRunnableCalled; + private DynamicConcatenatingMediaSource mediaSource; + private MediaSourceTestRunner testRunner; - public void testPlaylistChangesAfterPreparation() throws InterruptedException { - timeline = null; - FakeMediaSource[] childSources = createMediaSources(7); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); - prepareAndListenToTimelineUpdates(mediaSource); - assertNotNull(timeline); - waitForTimelineUpdate(); + @Override + public void setUp() { + mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + } + + @Override + public void tearDown() { + testRunner.release(); + } + + public void testPlaylistChangesAfterPreparation() { + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); + FakeMediaSource[] childSources = createMediaSources(7); + // Add first source. mediaSource.addMediaSource(childSources[0]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); // Add at front of queue. mediaSource.addMediaSource(0, childSources[1]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1); TimelineAsserts.assertWindowIds(timeline, 222, 111); // Add at back of queue. mediaSource.addMediaSource(childSources[2]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); // Add in the middle. mediaSource.addMediaSource(1, childSources[3]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); // Add bulk. - mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4], - (MediaSource) childSources[5], (MediaSource) childSources[6])); - waitForTimelineUpdate(); + mediaSource.addMediaSources(3, Arrays.asList(childSources[4], childSources[5], + childSources[6])); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Move sources. mediaSource.moveMediaSource(2, 3); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); mediaSource.moveMediaSource(3, 2); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); mediaSource.moveMediaSource(0, 6); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); mediaSource.moveMediaSource(6, 0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Remove in the middle. mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(1); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); for (int i = 3; i <= 6; i++) { @@ -146,35 +148,31 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(0, timeline.getLastWindowIndex(true)); // Assert all periods can be prepared. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); // Remove at front of queue. mediaSource.removeMediaSource(0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 3); TimelineAsserts.assertWindowIds(timeline, 111, 333); childSources[1].assertReleased(); // Remove at back of queue. mediaSource.removeMediaSource(1); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); childSources[2].assertReleased(); // Remove last source. mediaSource.removeMediaSource(0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); childSources[3].assertReleased(); } - public void testPlaylistChangesBeforePreparation() throws InterruptedException { - timeline = null; + public void testPlaylistChangesBeforePreparation() { FakeMediaSource[] childSources = createMediaSources(4); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); mediaSource.addMediaSource(0, childSources[2]); @@ -182,11 +180,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSource.removeMediaSource(0); mediaSource.moveMediaSource(1, 0); mediaSource.addMediaSource(1, childSources[3]); - assertNull(timeline); + testRunner.assertNoTimelineChange(); - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - assertNotNull(timeline); + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, @@ -198,98 +194,94 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); } } - public void testPlaylistWithLazyMediaSource() throws InterruptedException { - timeline = null; - + public void testPlaylistWithLazyMediaSource() { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); - FakeMediaSource[] lazySources = new FakeMediaSource[4]; + final FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { lazySources[i] = new FakeMediaSource(null, null); } // Add lazy sources and normal sources before preparation. Also remove one lazy source again // before preparation to check it doesn't throw or change the result. - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(lazySources[0]); mediaSource.addMediaSource(0, fastSources[0]); mediaSource.removeMediaSource(1); mediaSource.addMediaSource(1, lazySources[1]); - assertNull(timeline); + testRunner.assertNoTimelineChange(); // Prepare and assert that the timeline contains all information for normal sources while having // placeholder information for lazy sources. - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - assertNotNull(timeline); + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1); TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); // Trigger source info refresh for lazy source and check that the timeline now contains all // information for all windows. - lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); - waitForTimelineUpdate(); + testRunner.runOnPlaybackThread(new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); + timeline = testRunner.assertTimelineChange(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); // Add further lazy and normal sources after preparation. Also remove one lazy source again to // check it doesn't throw or change the result. mediaSource.addMediaSource(1, lazySources[2]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(2, fastSources[1]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(0, lazySources[3]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(2); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not // called yet. - 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 lazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + ConditionVariable preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); + assertFalse(preparedCondition.block(1)); + // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); - assertNotNull(secondLazyPeriod); - mediaSource.releasePeriod(secondLazyPeriod); + MediaPeriod secondLazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + testRunner.releasePeriod(secondLazyPeriod); // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. - lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); - waitForTimelineUpdate(); + testRunner.runOnPlaybackThread(new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); + timeline = testRunner.assertTimelineChange(); 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); + assertTrue(preparedCondition.block(1)); - // Release media source and assert all normal and lazy media sources are fully released as well. - mediaSource.releaseSource(); + // Release the period and source. + testRunner.releasePeriod(lazyPeriod); + testRunner.releaseSource(); + + // Assert all sources were fully released. for (FakeMediaSource fastSource : fastSources) { fastSource.assertReleased(); } @@ -298,17 +290,12 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineMediaSource() throws InterruptedException { - timeline = null; - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); - prepareAndListenToTimelineUpdates(mediaSource); - assertNotNull(timeline); - waitForTimelineUpdate(); + public void testEmptyTimelineMediaSource() { + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSources(Arrays.asList(new MediaSource[] { @@ -316,18 +303,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) })); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); 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(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(4, mediaSources[1]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(6, mediaSources[2]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, @@ -350,12 +337,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(2, timeline.getLastWindowIndex(false)); assertEquals(2, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); } public void testIllegalArguments() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); // Null sources. @@ -394,7 +379,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddSingle() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -402,7 +386,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddMultiple() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSources(Arrays.asList( @@ -411,7 +394,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); @@ -419,134 +401,159 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(/* index */ 0, Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + mediaSource.addMediaSources(/* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); verify(runnable).run(); } - public void testCustomCallbackBeforePreparationRemove() throws InterruptedException { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + public void testCustomCallbackBeforePreparationRemove() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSource(createFakeMediaSource()); + mediaSource.addMediaSource(createFakeMediaSource()); mediaSource.removeMediaSource(/* index */ 0, runnable); verify(runnable).run(); } - public void testCustomCallbackBeforePreparationMove() throws InterruptedException { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + public void testCustomCallbackBeforePreparationMove() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); verify(runnable).run(); } - public void testCustomCallbackAfterPreparationAddSingle() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddSingle() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(1, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddMultiple() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), - runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddSingleWithIndex() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(1, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources(/* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationRemove() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); - } - }); - waitForTimelineUpdate(); + public void testCustomCallbackAfterPreparationRemove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); - } - }); - waitForCustomRunnable(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(0, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationMove() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); - } - }); - waitForTimelineUpdate(); + public void testCustomCallbackAfterPreparationMove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + } + }); + testRunner.assertTimelineChangeBlocking(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, - runnable); - } - }); - waitForCustomRunnable(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } public void testPeriodCreationWithAds() throws InterruptedException { @@ -557,19 +564,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(mediaSourceContentOnly); mediaSource.addMediaSource(mediaSourceWithAds); - assertNull(timeline); - // Prepare and assert timeline contains ad groups. - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); + Timeline timeline = testRunner.prepareSource(); + + // Assert the timeline contains ad groups. TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); // Create all periods and assert period creation of child media sources has been called. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); @@ -578,66 +582,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } - private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() - throws InterruptedException { - final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); - } - - private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { - mediaSource.prepareSource(new MessageHandlingExoPlayer(), true, new Listener() { - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) { - timeline = newTimeline; - synchronized (DynamicConcatenatingMediaSourceTest.this) { - timelineUpdated = true; - DynamicConcatenatingMediaSourceTest.this.notify(); - } - } - }); - } - - private synchronized void waitForTimelineUpdate() throws InterruptedException { - long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; - while (!timelineUpdated) { - wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= deadlineMs) { - fail("No timeline update occurred within timeout."); - } - } - timelineUpdated = false; - } - - private Runnable createCustomRunnable() { - return new Runnable() { - @Override - public void run() { - synchronized (DynamicConcatenatingMediaSourceTest.this) { - assertTrue(timelineUpdated); - timelineUpdated = false; - customRunnableCalled = true; - DynamicConcatenatingMediaSourceTest.this.notify(); - } - } - }; - } - - private synchronized void waitForCustomRunnable() throws InterruptedException { - long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; - while (!customRunnableCalled) { - wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= deadlineMs) { - fail("No custom runnable call occurred within timeout."); - } - } - customRunnableCalled = false; - } - private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { @@ -654,48 +598,69 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } - private static class DynamicConcatenatingMediaSourceAndHandler { + private static final class DummyMainThread { - public final DynamicConcatenatingMediaSource mediaSource; - public final Handler mainHandler; + private final HandlerThread thread; + private final Handler handler; - public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler mainHandler) { - this.mediaSource = mediaSource; - this.mainHandler = mainHandler; + private DummyMainThread() { + thread = new HandlerThread("DummyMainThread"); + thread.start(); + handler = new Handler(thread.getLooper()); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + handler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + } + + public void release() { + thread.quit(); } } - /** - * ExoPlayer that only accepts custom messages and runs them on a separate handler thread. - */ - private static class MessageHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static final class TimelineGrabber implements Runnable { - private final Handler handler; + private final MediaSourceTestRunner testRunner; + private final ConditionVariable finishedCondition; - public MessageHandlingExoPlayer() { - HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper(), this); + private Timeline timeline; + private AssertionError error; + + public TimelineGrabber(MediaSourceTestRunner testRunner) { + this.testRunner = testRunner; + finishedCondition = new ConditionVariable(); } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); - } - - @Override - public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + public void run() { + try { + timeline = testRunner.assertTimelineChange(); + } catch (AssertionError e) { + error = e; } - return true; + finishedCondition.open(); + } + + public Timeline assertTimelineChangeBlocking() { + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (error != null) { + throw error; + } + return timeline; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java new file mode 100644 index 0000000000..df1282c7e1 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.testutil; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +/** + * A runner for {@link MediaSource} tests. + */ +public class MediaSourceTestRunner { + + private final long timeoutMs; + private final StubExoPlayer player; + private final MediaSource mediaSource; + private final MediaSourceListener mediaSourceListener; + private final HandlerThread playbackThread; + private final Handler playbackHandler; + private final Allocator allocator; + + private final LinkedBlockingDeque timelines; + private Timeline timeline; + + /** + * @param mediaSource The source under test. + * @param allocator The allocator to use during the test run. + * @param timeoutMs The timeout for operations in milliseconds. + */ + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + this.mediaSource = mediaSource; + this.allocator = allocator; + this.timeoutMs = timeoutMs; + playbackThread = new HandlerThread("PlaybackThread"); + playbackThread.start(); + Looper playbackLooper = playbackThread.getLooper(); + playbackHandler = new Handler(playbackLooper); + player = new EventHandlingExoPlayer(playbackLooper); + mediaSourceListener = new MediaSourceListener(); + timelines = new LinkedBlockingDeque<>(); + } + + /** + * Runs the provided {@link Runnable} on the playback thread, blocking until execution completes. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnPlaybackThread(final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + playbackHandler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertTrue(finishedCondition.block(timeoutMs)); + } + + /** + * Prepares the source on the playback thread, asserting that it provides an initial timeline. + * + * @return The initial {@link Timeline}. + */ + public Timeline prepareSource() { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(player, true, mediaSourceListener); + } + }); + return assertTimelineChangeBlocking(); + } + + /** + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback + * thread, asserting that a non-null {@link MediaPeriod} is returned. + * + * @param periodId The id of the period to create. + * @return The created {@link MediaPeriod}. + */ + public MediaPeriod createPeriod(final MediaPeriodId periodId) { + final MediaPeriod[] holder = new MediaPeriod[1]; + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + holder[0] = mediaSource.createPeriod(periodId, allocator); + } + }); + assertNotNull(holder[0]); + return holder[0]; + } + + /** + * Calls {@link MediaPeriod#prepare(MediaPeriod.Callback, long)} on the playback thread. + * + * @param mediaPeriod The {@link MediaPeriod} to prepare. + * @param positionUs The position at which to prepare. + * @return A {@link ConditionVariable} that will be opened when preparation completes. + */ + public ConditionVariable preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) { + final ConditionVariable preparedCondition = new ConditionVariable(); + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaPeriod.prepare(new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + preparedCondition.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Do nothing. + } + }, positionUs); + } + }); + return preparedCondition; + } + + /** + * Calls {@link MediaSource#releasePeriod(MediaPeriod)} on the playback thread. + * + * @param mediaPeriod The {@link MediaPeriod} to release. + */ + public void releasePeriod(final MediaPeriod mediaPeriod) { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.releasePeriod(mediaPeriod); + } + }); + } + + /** + * Calls {@link MediaSource#releaseSource()} on the playback thread. + */ + public void releaseSource() { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.releaseSource(); + } + }); + } + + /** + * Asserts that the source has not notified its listener of a timeline change since the last call + * to {@link #assertTimelineChangeBlocking()} or {@link #assertTimelineChange()} (or since the + * runner was created if neither method has been called). + */ + public void assertNoTimelineChange() { + assertTrue(timelines.isEmpty()); + } + + /** + * Asserts that the source has notified its listener of a single timeline change. + * + * @return The new {@link Timeline}. + */ + public Timeline assertTimelineChange() { + timeline = timelines.removeFirst(); + assertNoTimelineChange(); + return timeline; + } + + /** + * Asserts that the source notifies its listener of a single timeline change. If the source has + * not yet notified its listener, it has up to the timeout passed to the constructor to do so. + * + * @return The new {@link Timeline}. + */ + public Timeline assertTimelineChangeBlocking() { + try { + timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + assertNotNull(timeline); // Null indicates the poll timed out. + assertNoTimelineChange(); + return timeline; + } catch (InterruptedException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + /** + * Creates and releases all periods (including ad periods) defined in the last timeline to be + * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or + * {@link #assertTimelineChangeBlocking()}. + */ + public void assertPrepareAndReleaseAllPeriods() { + Timeline.Period period = new Timeline.Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + assertPrepareAndReleasePeriod(new MediaPeriodId(i)); + timeline.getPeriod(i, period); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + assertPrepareAndReleasePeriod(new MediaPeriodId(i, adGroupIndex, adIndex)); + } + } + } + } + + private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { + MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); + ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); + assertTrue(preparedCondition.block(timeoutMs)); + // MediaSource is supposed to support multiple calls to createPeriod with the same id without an + // intervening call to releasePeriod. + MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); + ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); + assertTrue(secondPreparedCondition.block(timeoutMs)); + // Release the periods. + releasePeriod(mediaPeriod); + releasePeriod(secondMediaPeriod); + } + + /** + * Releases the runner. Should be called when the runner is no longer required. + */ + public void release() { + playbackThread.quit(); + } + + private class MediaSourceListener implements MediaSource.Listener { + + @Override + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + timelines.addLast(timeline); + } + + } + + private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + + private final Handler handler; + + public EventHandlingExoPlayer(Looper looper) { + this.handler = new Handler(looper, this); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + handler.obtainMessage(0, messages).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; + for (ExoPlayerMessage message : messages) { + try { + message.target.handleMessage(message.messageType, message.message); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); + } + } + return true; + } + + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 61d1ecaeea..9ee181024c 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 @@ -146,6 +146,7 @@ public class TestUtil { /** * Extracts the timeline from a media source. */ + // TODO: Remove this method and transition callers over to MediaSourceTestRunner. public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { class TimelineListener implements Listener { private Timeline timeline; 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 b1df8f62e1..62af44f32f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -16,19 +16,11 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertTrue; - -import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaPeriod.Callback; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; /** * Unit test for {@link Timeline}. @@ -157,46 +149,4 @@ public final class TimelineAsserts { } } - /** - * Asserts that all period (including ad periods) can be created from the source, prepared, and - * released without exception and within timeout. - */ - public static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - Timeline timeline, long timeoutMs) { - Period period = new Period(); - for (int i = 0; i < timeline.getPeriodCount(); i++) { - assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, new MediaPeriodId(i), timeoutMs); - timeline.getPeriod(i, period); - for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { - for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { - assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, - new MediaPeriodId(i, adGroupIndex, adIndex), timeoutMs); - } - } - } - } - - private static void assertPeriodCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - MediaPeriodId mediaPeriodId, long timeoutMs) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(mediaPeriodId, 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) {} - }, /* positionUs= */ 0); - assertTrue(mediaPeriodPrepared.block(timeoutMs)); - // MediaSource is supposed to support multiple calls to createPeriod with the same id without an - // intervening call to releasePeriod. - MediaPeriod secondMediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); - assertNotNull(secondMediaPeriod); - mediaSource.releasePeriod(secondMediaPeriod); - mediaSource.releasePeriod(mediaPeriod); - } - } - From 09f3055badc4ac99cd3a7639cef9a099891483a9 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 20 Nov 2017 02:41:38 -0800 Subject: [PATCH 064/148] Add Builder to SingleSampleMediaSource. Add Builder pattern to SingleSampleMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176332964 --- RELEASENOTES.md | 2 +- .../source/SingleSampleMediaSource.java | 107 ++++++++++++++++++ .../exoplayer2/upstream/DummyDataSource.java | 2 +- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a5ccb583c..6683ee3f55 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,7 +3,7 @@ ### dev-v2 (not yet released) ### * Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource. + DashMediaSource, SingleSampleMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index dd901958fd..2aa8ccc712 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -45,6 +45,107 @@ public final class SingleSampleMediaSource implements MediaSource { } + /** + * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final Format format; + private final long durationUs; + + private int minLoadableRetryCount; + private Handler eventHandler; + private EventListener eventListener; + private int eventSourceId; + private boolean treatLoadErrorsAsEndOfStream; + private boolean isBuildCalled; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + */ + public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + this.format = format; + this.durationUs = durationUs; + this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the listener to respond to events and the handler to deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. + * + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @return This builder. + */ + public Builder setEventSourceId(int eventSourceId) { + this.eventSourceId = eventSourceId; + return this; + } + + /** + * Sets whether load errors will be treated as end-of-stream signal (load errors will not be + * propagated). The default value is false. + * + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated + * normally by {@link SampleStream#maybeThrowError()}. + * @return This builder. + */ + public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + return this; + } + + /** + * Builds a new {@link SingleSampleMediaSource} using the current parameters. + *

      + * After this call, the builder should not be re-used. + * + * @return The newly built {@link SingleSampleMediaSource}. + */ + public SingleSampleMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + + return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, + minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + treatLoadErrorsAsEndOfStream); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -66,7 +167,9 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); @@ -79,7 +182,9 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount) { this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); @@ -98,7 +203,9 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java index c20868ef00..fa3e14f1c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -25,7 +25,7 @@ public final class DummyDataSource implements DataSource { public static final DummyDataSource INSTANCE = new DummyDataSource(); - /** A factory that that produces {@link DummyDataSource}. */ + /** A factory that produces {@link DummyDataSource}. */ public static final Factory FACTORY = new Factory() { @Override public DataSource createDataSource() { From dc425a942c3cc6181113a3aa0549665af64825ea Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 02:49:39 -0800 Subject: [PATCH 065/148] Use consistent case for sideloaded ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176333544 --- .../google/android/exoplayer2/source/dash/DashMediaSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 54a5086d3b..02f928544b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -83,7 +83,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @return A new builder. */ - public static Builder forSideLoadedManifest(DashManifest manifest, + public static Builder forSideloadedManifest(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory) { Assertions.checkArgument(!manifest.dynamic); return new Builder(manifest, null, null, chunkSourceFactory); From 2ad87f4f5b08a8f201b2ed750ac028933aaf46ff Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 03:17:29 -0800 Subject: [PATCH 066/148] Add time unit and javadocs to fields in DashManifest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176335667 --- .../dash/manifest/DashManifestTest.java | 12 +-- .../source/dash/DashMediaSource.java | 28 +++---- .../source/dash/DefaultDashChunkSource.java | 6 +- .../source/dash/manifest/DashManifest.java | 75 +++++++++++++------ 4 files changed, 77 insertions(+), 44 deletions(-) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index 7d77ae82d9..dfcb9e72a5 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -129,13 +129,13 @@ public class DashManifestTest extends TestCase { } private static void assertManifestEquals(DashManifest expected, DashManifest actual) { - assertEquals(expected.availabilityStartTime, actual.availabilityStartTime); - assertEquals(expected.duration, actual.duration); - assertEquals(expected.minBufferTime, actual.minBufferTime); + assertEquals(expected.availabilityStartTimeMs, actual.availabilityStartTimeMs); + assertEquals(expected.durationMs, actual.durationMs); + assertEquals(expected.minBufferTimeMs, actual.minBufferTimeMs); assertEquals(expected.dynamic, actual.dynamic); - assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod); - assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth); - assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay); + assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); + assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); + assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 02f928544b..a82b5af583 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -199,7 +199,7 @@ public final class DashMediaSource implements MediaSource { public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; /** * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or + * {@link DashManifest#suggestedPresentationDelayMs} if specified by the manifest, or * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the * duration by which the default start position precedes the end of the live window. */ @@ -626,12 +626,12 @@ public final class DashMediaSource implements MediaSource { if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { // The manifest describes an incomplete live stream. Update the start/end times to reflect the // live stream duration and the manifest's time shift buffer depth. - long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); - if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); + if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs; int periodIndex = lastPeriodIndex; while (offsetInPeriodUs < 0 && periodIndex > 0) { @@ -655,8 +655,8 @@ public final class DashMediaSource implements MediaSource { if (manifest.dynamic) { long presentationDelayForManifestMs = livePresentationDelayMs; if (presentationDelayForManifestMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) { - presentationDelayForManifestMs = manifest.suggestedPresentationDelay != C.TIME_UNSET - ? manifest.suggestedPresentationDelay : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; + presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs != C.TIME_UNSET + ? manifest.suggestedPresentationDelayMs : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; } // Snap the default position to the start of the segment containing it. windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); @@ -668,9 +668,9 @@ public final class DashMediaSource implements MediaSource { windowDurationUs / 2); } } - long windowStartTimeMs = manifest.availabilityStartTime + long windowStartTimeMs = manifest.availabilityStartTimeMs + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs); - DashTimeline timeline = new DashTimeline(manifest.availabilityStartTime, windowStartTimeMs, + DashTimeline timeline = new DashTimeline(manifest.availabilityStartTimeMs, windowStartTimeMs, firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs, manifest); sourceListener.onSourceInfoRefreshed(this, timeline, manifest); @@ -693,15 +693,15 @@ public final class DashMediaSource implements MediaSource { if (!manifest.dynamic) { return; } - long minUpdatePeriod = manifest.minUpdatePeriod; - if (minUpdatePeriod == 0) { + long minUpdatePeriodMs = manifest.minUpdatePeriodMs; + if (minUpdatePeriodMs == 0) { // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where - // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit - // signaling in the stream, according to: + // minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is + // explicit signaling in the stream, according to: // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ - minUpdatePeriod = 5000; + minUpdatePeriodMs = 5000; } - long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod; + long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriodMs; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad); } 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 66455b2f04..b254c4f09a 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 @@ -220,11 +220,11 @@ public class DefaultDashChunkSource implements DashChunkSource { if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; - if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); + if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index cd02e27fce..6cc9397596 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -23,41 +23,74 @@ import java.util.LinkedList; import java.util.List; /** - * Represents a DASH media presentation description (mpd). + * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014 + * Section 5.3.1.2. */ public class DashManifest { - public final long availabilityStartTime; + /** + * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long availabilityStartTimeMs; - public final long duration; + /** + * The duration of the presentation in milliseconds, or {@link C#TIME_UNSET} if not applicable. + */ + public final long durationMs; - public final long minBufferTime; + /** + * The {@code minBufferTime} value in milliseconds, or {@link C#TIME_UNSET} if not present. + */ + public final long minBufferTimeMs; + /** + * Whether the manifest has value "dynamic" for the {@code type} attribute. + */ public final boolean dynamic; - public final long minUpdatePeriod; + /** + * The {@code minimumUpdatePeriod} value in milliseconds, or {@link C#TIME_UNSET} if not + * applicable. + */ + public final long minUpdatePeriodMs; - public final long timeShiftBufferDepth; + /** + * The {@code timeShiftBufferDepth} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long timeShiftBufferDepthMs; - public final long suggestedPresentationDelay; + /** + * The {@code suggestedPresentationDelay} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long suggestedPresentationDelayMs; + /** + * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section + * 4.7.2. + */ public final UtcTimingElement utcTiming; + /** + * The location of this manifest. + */ public final Uri location; private final List periods; - public DashManifest(long availabilityStartTime, long duration, long minBufferTime, - boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, - long suggestedPresentationDelay, UtcTimingElement utcTiming, Uri location, + public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, + boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, List periods) { - this.availabilityStartTime = availabilityStartTime; - this.duration = duration; - this.minBufferTime = minBufferTime; + this.availabilityStartTimeMs = availabilityStartTimeMs; + this.durationMs = durationMs; + this.minBufferTimeMs = minBufferTimeMs; this.dynamic = dynamic; - this.minUpdatePeriod = minUpdatePeriod; - this.timeShiftBufferDepth = timeShiftBufferDepth; - this.suggestedPresentationDelay = suggestedPresentationDelay; + this.minUpdatePeriodMs = minUpdatePeriodMs; + this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; + this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -73,7 +106,7 @@ public class DashManifest { public final long getPeriodDurationMs(int index) { return index == periods.size() - 1 - ? (duration == C.TIME_UNSET ? C.TIME_UNSET : (duration - periods.get(index).startMs)) + ? (durationMs == C.TIME_UNSET ? C.TIME_UNSET : (durationMs - periods.get(index).startMs)) : (periods.get(index + 1).startMs - periods.get(index).startMs); } @@ -110,10 +143,10 @@ public class DashManifest { copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); } } - long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; - return new DashManifest(availabilityStartTime, newDuration, minBufferTime, dynamic, - minUpdatePeriod, timeShiftBufferDepth, suggestedPresentationDelay, utcTiming, location, - copyPeriods); + long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; + return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, + location, copyPeriods); } private static ArrayList copyAdaptationSets( From 82ce68009cf84e681272cd628289b91e06af0097 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:20:52 -0800 Subject: [PATCH 067/148] Move MockitoUtils to testutils and use it for all Mockito set-ups. In particular this allows to have the workaround for https://code.google.com/p/dexmaker/issues/detail?id=2 in one place only. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176340526 --- extensions/cronet/build.gradle | 1 + .../ByteArrayUploadDataProviderTest.java | 6 ++---- .../ext/cronet/CronetDataSourceTest.java | 6 ++---- .../drm/OfflineLicenseHelperTest.java | 14 ++------------ .../cache/CachedRegionTrackerTest.java | 14 ++------------ .../dash/offline/DashDownloaderTest.java | 2 +- .../exoplayer2/testutil}/MockitoUtil.java | 18 +++++++++++++++++- 7 files changed, 27 insertions(+), 34 deletions(-) rename {library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash => testutils/src/main/java/com/google/android/exoplayer2/testutil}/MockitoUtil.java (65%) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 197dec80a5..0b6f9a587c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -40,6 +40,7 @@ dependencies { compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') androidTestCompile project(modulePrefix + 'library') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index a65bb0951b..bd81750fcb 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest { @Before public void setUp() { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); } diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 4c6a42849f..f92574b7ab 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import android.net.Uri; import android.os.ConditionVariable; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; @@ -107,9 +107,7 @@ public final class CronetDataSourceTest { @Before public void setUp() throws Exception { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); dataSourceUnderTest = spy( new CronetDataSource( mockCronetEngine, 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 22ae57932b..02b29a31b5 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,9 +23,9 @@ 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.MockitoUtil; import java.util.HashMap; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests {@link OfflineLicenseHelper}. @@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); @@ -156,14 +156,4 @@ 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/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 472b5c724b..f40ae0bc7e 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.MockitoUtil; 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,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); @@ -123,14 +123,4 @@ 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/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 8532e65a68..ec0292514a 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,12 +27,12 @@ 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; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java similarity index 65% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java index e7cd9baf59..6bd1048bc0 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source.dash; +package com.google.android.exoplayer2.testutil; +import android.content.Context; import android.test.InstrumentationTestCase; import org.mockito.MockitoAnnotations; @@ -25,6 +26,8 @@ public final class MockitoUtil { /** * Sets up Mockito for an instrumentation test. + * + * @param instrumentationTestCase The instrumentation test case class. */ public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. @@ -33,6 +36,19 @@ public final class MockitoUtil { MockitoAnnotations.initMocks(instrumentationTestCase); } + /** + * Sets up Mockito for a JUnit4 test. + * + * @param targetContext The target context. Usually obtained from + * {@code InstrumentationRegistry.getTargetContext()} + * @param testClass The JUnit4 test class. + */ + public static void setUpMockito(Context targetContext, Object testClass) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", targetContext.getCacheDir().getPath()); + MockitoAnnotations.initMocks(testClass); + } + private MockitoUtil() {} } From c3b92f84562aa55dc79d764f6fa4cd2e827f39e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 04:32:56 -0800 Subject: [PATCH 068/148] Add support for Dolby Atmos Issue: #2465 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176341309 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/Ac3Util.java | 170 +++++++++++++++++- .../exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 60 +++++-- .../android/exoplayer2/util/MimeTypes.java | 4 + .../dash/manifest/DashManifestParser.java | 22 ++- 6 files changed, 237 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6683ee3f55..f772bc9f19 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ DashMediaSource, SingleSampleMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. +* Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e1a70e2579..e9ffab7ace 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -181,7 +185,14 @@ public final class Ac3Util { channelCount += 2; } } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, + String mimeType = MimeTypes.AUDIO_E_AC3; + if (data.bytesLeft() > 0) { + nextByte = data.readUnsignedByte(); + if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a + mimeType = MimeTypes.AUDIO_ATMOS; + } + } + return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); } @@ -198,29 +209,176 @@ public final class Ac3Util { boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; - int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + int streamType = STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; int sampleCount; + boolean lfeon; + int channelCount; if (isEac3) { - mimeType = MimeTypes.AUDIO_E_AC3; + // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. data.skipBits(16); // syncword streamType = data.readBits(2); data.skipBits(3); // substreamid frameSize = (data.readBits(11) + 1) * 2; int fscod = data.readBits(2); int audioBlocks; + int numblkscod; if (fscod == 3) { + numblkscod = 3; sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; audioBlocks = 6; } else { - int numblkscod = data.readBits(2); + numblkscod = data.readBits(2); audioBlocks = BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod]; sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; } sampleCount = AUDIO_SAMPLES_PER_AUDIO_BLOCK * audioBlocks; acmod = data.readBits(3); + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + data.skipBits(5 + 5); // bsid, dialnorm + if (data.readBit()) { // compre + data.skipBits(8); // compr + } + if (acmod == 0) { + data.skipBits(5); // dialnorm2 + if (data.readBit()) { // compr2e + data.skipBits(8); // compr2 + } + } + if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape + data.skipBits(16); // chanmap + } + if (data.readBit()) { // mixmdate + if (acmod > 2) { + data.skipBits(2); // dmixmod + } + if ((acmod & 0x01) != 0 && acmod > 2) { + data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(6); // ltrtsurmixlev, lorosurmixlev + } + if (lfeon && data.readBit()) { // lfemixlevcode + data.skipBits(5); // lfemixlevcod + } + if (streamType == STREAM_TYPE_TYPE0) { + if (data.readBit()) { // pgmscle + data.skipBits(6); //pgmscl + } + if (acmod == 0 && data.readBit()) { // pgmscl2e + data.skipBits(6); // pgmscl2 + } + if (data.readBit()) { // extpgmscle + data.skipBits(6); // extpgmscl + } + int mixdef = data.readBits(2); + if (mixdef == 1) { + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + } else if (mixdef == 2) { + data.skipBits(12); // mixdata + } else if (mixdef == 3) { + int mixdeflen = data.readBits(5); + if (data.readBit()) { // mixdata2e + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + if (data.readBit()) { // extpgmlscle + data.skipBits(4); // extpgmlscl + } + if (data.readBit()) { // extpgmcscle + data.skipBits(4); // extpgmcscl + } + if (data.readBit()) { // extpgmrscle + data.skipBits(4); // extpgmrscl + } + if (data.readBit()) { // extpgmlsscle + data.skipBits(4); // extpgmlsscl + } + if (data.readBit()) { // extpgmrsscle + data.skipBits(4); // extpgmrsscl + } + if (data.readBit()) { // extpgmlfescle + data.skipBits(4); // extpgmlfescl + } + if (data.readBit()) { // dmixscle + data.skipBits(4); // dmixscl + } + if (data.readBit()) { // addche + if (data.readBit()) { // extpgmaux1scle + data.skipBits(4); // extpgmaux1scl + } + if (data.readBit()) { // extpgmaux2scle + data.skipBits(4); // extpgmaux2scl + } + } + } + if (data.readBit()) { // mixdata3e + data.skipBits(5); // spchdat + if (data.readBit()) { // addspchdate + data.skipBits(5 + 2); // spchdat1, spchan1att + if (data.readBit()) { // addspdat1e + data.skipBits(5 + 3); // spchdat2, spchan2att + } + } + } + data.skipBits(8 * (mixdeflen + 2)); // mixdata + data.byteAlign(); // mixdatafill + } + if (acmod < 2) { + if (data.readBit()) { // paninfoe + data.skipBits(8 + 6); // panmean, paninfo + } + if (acmod == 0) { + if (data.readBit()) { // paninfo2e + data.skipBits(8 + 6); // panmean2, paninfo2 + } + } + } + if (data.readBit()) { // frmmixcfginfoe + if (numblkscod == 0) { + data.skipBits(5); // blkmixcfginfo[0] + } else { + for (int blk = 0; blk < audioBlocks; blk++) { + if (data.readBit()) { // blkmixcfginfoe + data.skipBits(5); // blkmixcfginfo[blk] + } + } + } + } + } + } + if (data.readBit()) { // infomdate + data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs + if (acmod == 2) { + data.skipBits(2 + 2); // dsurmod, dheadphonmod + } + if (acmod >= 6) { + data.skipBits(2); // dsurexmod + } + if (data.readBit()) { // audioprodie + data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp + } + if (acmod == 0 && data.readBit()) { // audioprodi2e + data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2 + } + if (fscod < 3) { + data.skipBit(); // sourcefscod + } + } + if (streamType == 0 && numblkscod != 3) { + data.skipBit(); // convsync + } + if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid + data.skipBits(6); // frmsizecod + } + mimeType = MimeTypes.AUDIO_E_AC3; + if (data.readBit()) { // addbsie + int addbsil = data.readBits(6); + if (addbsil == 1 && data.readBits(8) == 1) { // addbsi + mimeType = MimeTypes.AUDIO_ATMOS; + } + } } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 @@ -240,9 +398,9 @@ public final class Ac3Util { } sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } - boolean lfeon = data.readBit(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 6a1c566faf..8383bfb8d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 8; + private static final int HEADER_SIZE = 128; private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; 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 f75ce5a9e5..7ae8eb3cd4 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 @@ -20,6 +20,7 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -120,7 +121,7 @@ public final class MediaCodecUtil { * exists. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) + public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) throws DecoderQueryException { List decoderInfos = getDecoderInfos(mimeType, secure); return decoderInfos.isEmpty() ? null : decoderInfos.get(0); @@ -140,27 +141,34 @@ public final class MediaCodecUtil { public static synchronized List getDecoderInfos(String mimeType, boolean secure) throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); - List decoderInfos = decoderInfosCache.get(key); - if (decoderInfos != null) { - return decoderInfos; + List cachedDecoderInfos = decoderInfosCache.get(key); + if (cachedDecoderInfos != null) { + return cachedDecoderInfos; } MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); } } + if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) { + // E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D. + CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure); + ArrayList eac3DecoderInfos = + getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); + decoderInfos.addAll(eac3DecoderInfos); + } applyWorkarounds(decoderInfos); - decoderInfos = Collections.unmodifiableList(decoderInfos); - decoderInfosCache.put(key, decoderInfos); - return decoderInfos; + List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, unmodifiableDecoderInfos); + return unmodifiableDecoderInfos; } /** @@ -212,10 +220,21 @@ public final class MediaCodecUtil { // Internal methods. - private static List getDecoderInfosInternal( - CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + /** + * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * {@code mediaCodecList}. + * + * @param key The codec key. + * @param mediaCodecList The codec list. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return The codec information for usable codecs matching the specified key. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + private static ArrayList getDecoderInfosInternal(CodecKey key, + MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { try { - List decoderInfos = new ArrayList<>(); + ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); @@ -223,7 +242,7 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String codecName = codecInfo.getName(); - if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { + if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) { for (String supportedType : codecInfo.getSupportedTypes()) { if (supportedType.equalsIgnoreCase(mimeType)) { try { @@ -265,9 +284,16 @@ public final class MediaCodecUtil { /** * Returns whether the specified codec is usable for decoding on the current device. + * + * @param info The codec information. + * @param name The name of the codec + * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return Whether the specified codec is usable for decoding on the current device. */ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit) { + boolean secureDecodersExplicit, String requestedMimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -356,6 +382,12 @@ public final class MediaCodecUtil { return false; } + // MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041]. + if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType) + && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + return false; + } + return true; } 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 c29a4c3717..a68e0142d6 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 @@ -51,6 +51,7 @@ public final class MimeTypes { public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_ATMOS = BASE_TYPE_AUDIO + "/eac3-joc"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -195,6 +196,8 @@ public final class MimeTypes { return MimeTypes.AUDIO_AC3; } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { return MimeTypes.AUDIO_E_AC3; + } else if (codec.startsWith("ec+3")) { + return MimeTypes.AUDIO_ATMOS; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -252,6 +255,7 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_ATMOS: return C.ENCODING_E_AC3; case MimeTypes.AUDIO_DTS: return C.ENCODING_DTS; 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 137e29c5ab..aa4c6b1e30 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 @@ -452,6 +452,7 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -479,12 +480,14 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, - adaptationSetAccessibilityDescriptors, codecs); + adaptationSetAccessibilityDescriptors, codecs, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, @@ -494,9 +497,12 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, - String codecs) { + String codecs, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } if (MimeTypes.isVideo(sampleMimeType)) { return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, width, height, frameRate, null, selectionFlags); @@ -900,6 +906,18 @@ public class DashManifestParser extends DefaultHandler return Format.NO_VALUE; } + protected static String parseEac3SupplementalProperties(List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + String schemeIdUri = descriptor.schemeIdUri; + if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value)) { + return MimeTypes.AUDIO_ATMOS; + } + } + return MimeTypes.AUDIO_E_AC3; + } + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { float frameRate = defaultValue; String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); From 790316688657c5d9b7fa8a2a6bce58a1ee833f39 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 06:22:54 -0800 Subject: [PATCH 069/148] Allow human readable strings as DRM intent extras. Issue:#3478 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176351086 --- .../android/exoplayer2/demo/DemoUtil.java | 31 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 12 ++++--- .../demo/SampleChooserActivity.java | 27 +++++----------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index f9e9c34158..5ff7c5cb40 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -16,14 +16,43 @@ package com.google.android.exoplayer2.demo; import android.text.TextUtils; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.util.Locale; +import java.util.UUID; /** * Utility methods for demo application. */ -/*package*/ final class DemoUtil { +/* package */ final class DemoUtil { + + /** + * Derives a DRM {@link UUID} from {@code drmScheme}. + * + * @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or + * {@code "clearkey"}. + * @return The derived {@link UUID}. + * @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}. + */ + public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException { + switch (Util.toLowerInvariant(drmScheme)) { + case "widevine": + return C.WIDEVINE_UUID; + case "playready": + return C.PLAYREADY_UUID; + case "clearkey": + return C.CLEARKEY_UUID; + default: + try { + return UUID.fromString(drmScheme); + } catch (RuntimeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); + } + } + } /** * Builds a track name for display. 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 ca253db809..efde775176 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 @@ -83,7 +83,7 @@ import java.util.UUID; public class PlayerActivity extends Activity implements OnClickListener, PlaybackControlView.VisibilityListener { - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; 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"; @@ -98,6 +98,9 @@ public class PlayerActivity extends Activity implements OnClickListener, public static final String EXTENSION_LIST_EXTRA = "extension_list"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + // For backwards compatibility. + private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { @@ -256,10 +259,8 @@ public class PlayerActivity extends Activity implements OnClickListener, lastSeenTrackGroupArray = null; eventLogger = new EventLogger(trackSelector); - UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) - ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; - if (drmSchemeUuid != null) { + if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); @@ -268,6 +269,9 @@ public class PlayerActivity extends Activity implements OnClickListener, errorStringId = R.string.error_drm_not_supported; } else { try { + String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA + : DRM_SCHEME_UUID_EXTRA; + UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { 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 1f84b1f29c..308bab2a3b 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 @@ -32,8 +32,8 @@ import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity { break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmUuid = getDrmUuid(reader.nextString()); + try { + drmUuid = DemoUtil.getDrmUuid(reader.nextString()); + } catch (UnsupportedDrmException e) { + throw new ParserException(e); + } break; case "drm_license_url": Assertions.checkState(!insidePlaylist, @@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity { return group; } - private UUID getDrmUuid(String typeString) throws ParserException { - switch (Util.toLowerInvariant(typeString)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "clearkey": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(typeString); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + typeString); - } - } - } - } private static final class SampleAdapter extends BaseExpandableListAdapter { @@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity { public void updateIntent(Intent intent) { Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); + intent.putExtra(PlayerActivity.DRM_SCHEME_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); From 5c0e1f3e8aa057d419d87db403bf7edaf5438563 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:48:10 -0800 Subject: [PATCH 070/148] Use MediaSourceTestRunner in additional source tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366471 --- .../source/ClippingMediaSourceTest.java | 13 +++++--- .../source/ConcatenatingMediaSourceTest.java | 12 ++++--- .../DynamicConcatenatingMediaSourceTest.java | 14 ++++----- .../source/LoopingMediaSourceTest.java | 14 ++++++--- .../testutil/MediaSourceTestRunner.java | 30 ++++++++++++------ .../android/exoplayer2/testutil/TestUtil.java | 31 ------------------- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 5e615dbc7f..3c870f06f4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; /** @@ -123,9 +123,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new ClippingMediaSource(mediaSource, startMs, endMs)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } 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 429325defc..1ca32be46d 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 @@ -24,7 +24,6 @@ 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.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -33,8 +32,6 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -211,7 +208,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); @@ -241,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - return TestUtil.extractTimelineFromMediaSource(mediaSource); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 536180fafc..16c9e1a17c 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 @@ -39,19 +39,19 @@ import org.mockito.Mockito; */ public final class DynamicConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - private DynamicConcatenatingMediaSource mediaSource; private MediaSourceTestRunner testRunner; @Override - public void setUp() { + public void setUp() throws Exception { + super.setUp(); mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + testRunner = new MediaSourceTestRunner(mediaSource, null); } @Override - public void tearDown() { + public void tearDown() throws Exception { + super.tearDown(); testRunner.release(); } @@ -623,7 +623,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { finishedCondition.open(); } }); - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); } public void release() { @@ -656,7 +656,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public Timeline assertTimelineChangeBlocking() { - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); if (error != null) { throw error; } 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 79f646b5c4..6f69923ea2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -110,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase { * the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new LoopingMediaSource(mediaSource, loopCount)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } - diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index df1282c7e1..235c04bef5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -39,7 +41,8 @@ import java.util.concurrent.TimeUnit; */ public class MediaSourceTestRunner { - private final long timeoutMs; + public static final int TIMEOUT_MS = 10000; + private final StubExoPlayer player; private final MediaSource mediaSource; private final MediaSourceListener mediaSourceListener; @@ -53,12 +56,10 @@ public class MediaSourceTestRunner { /** * @param mediaSource The source under test. * @param allocator The allocator to use during the test run. - * @param timeoutMs The timeout for operations in milliseconds. */ - public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator) { this.mediaSource = mediaSource; this.allocator = allocator; - this.timeoutMs = timeoutMs; playbackThread = new HandlerThread("PlaybackThread"); playbackThread.start(); Looper playbackLooper = playbackThread.getLooper(); @@ -74,15 +75,24 @@ public class MediaSourceTestRunner { * @param runnable The {@link Runnable} to run. */ public void runOnPlaybackThread(final Runnable runnable) { + final Throwable[] throwable = new Throwable[1]; final ConditionVariable finishedCondition = new ConditionVariable(); playbackHandler.post(new Runnable() { @Override public void run() { - runnable.run(); - finishedCondition.open(); + try { + runnable.run(); + } catch (Throwable e) { + throwable[0] = e; + } finally { + finishedCondition.open(); + } } }); - assertTrue(finishedCondition.block(timeoutMs)); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (throwable[0] != null) { + Util.sneakyThrow(throwable[0]); + } } /** @@ -200,7 +210,7 @@ public class MediaSourceTestRunner { */ public Timeline assertTimelineChangeBlocking() { try { - timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + timeline = timelines.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(timeline); // Null indicates the poll timed out. assertNoTimelineChange(); return timeline; @@ -231,12 +241,12 @@ public class MediaSourceTestRunner { private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); - assertTrue(preparedCondition.block(timeoutMs)); + assertTrue(preparedCondition.block(TIMEOUT_MS)); // MediaSource is supposed to support multiple calls to createPeriod with the same id without an // intervening call to releasePeriod. MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); - assertTrue(secondPreparedCondition.block(timeoutMs)); + assertTrue(secondPreparedCondition.block(TIMEOUT_MS)); // Release the periods. releasePeriod(mediaPeriod); releasePeriod(secondMediaPeriod); 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 9ee181024c..d10b8a8269 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -19,10 +19,7 @@ import android.app.Instrumentation; import android.content.Context; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -143,34 +140,6 @@ public class TestUtil { return new String(getByteArray(instrumentation, fileName)); } - /** - * Extracts the timeline from a media source. - */ - // TODO: Remove this method and transition callers over to MediaSourceTestRunner. - public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { - class TimelineListener implements Listener { - private Timeline timeline; - @Override - public synchronized void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - this.timeline = timeline; - this.notify(); - } - } - TimelineListener listener = new TimelineListener(); - mediaSource.prepareSource(null, true, listener); - synchronized (listener) { - while (listener.timeline == null) { - try { - listener.wait(); - } catch (InterruptedException e) { - Assert.fail(e.getMessage()); - } - } - } - return listener.timeline; - } - /** * Asserts that data read from a {@link DataSource} matches {@code expected}. * From fa5cc5be5104094a7368cdbc039351cc1003f418 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:50:09 -0800 Subject: [PATCH 071/148] Bump target API level to 27 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366693 --- constants.gradle | 4 ++-- demos/ima/src/main/AndroidManifest.xml | 2 +- demos/main/src/main/AndroidManifest.xml | 2 +- extensions/cronet/src/androidTest/AndroidManifest.xml | 2 +- extensions/flac/src/androidTest/AndroidManifest.xml | 2 +- extensions/opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- library/core/src/androidTest/AndroidManifest.xml | 2 +- library/dash/src/androidTest/AndroidManifest.xml | 2 +- library/hls/src/androidTest/AndroidManifest.xml | 2 +- library/smoothstreaming/src/androidTest/AndroidManifest.xml | 2 +- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/constants.gradle b/constants.gradle index 2a7754d65c..bad69389a5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -17,8 +17,8 @@ project.ext { // However, please note that the core media playback functionality provided // by the library requires API level 16 or greater. minSdkVersion = 14 - compileSdkVersion = 26 - targetSdkVersion = 26 + compileSdkVersion = 27 + targetSdkVersion = 27 buildToolsVersion = '26.0.2' testSupportLibraryVersion = '0.5' supportLibraryVersion = '27.0.0' diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5252d2feeb..f14feeda74 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:versionName="2.6.0"> - + diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index d041e24d80..ec8016e8a3 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + - + - + - + - + - + - + - + - + - + Date: Tue, 21 Nov 2017 10:16:50 -0800 Subject: [PATCH 072/148] Parse DASH manifest's publish time. Parse DASH manifest's publishTime node as defined by ISO/IEC 23009-1:2014, section 5.3.1.2. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176525922 --- .../source/dash/manifest/DashManifestTest.java | 3 ++- .../source/dash/manifest/DashManifest.java | 15 +++++++++++---- .../source/dash/manifest/DashManifestParser.java | 13 +++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index dfcb9e72a5..882b0eb374 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -136,6 +136,7 @@ public class DashManifestTest extends TestCase { assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); + assertEquals(expected.publishTimeMs, actual.publishTimeMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); @@ -179,7 +180,7 @@ public class DashManifestTest extends TestCase { } private static DashManifest newDashManifest(int duration, Period... periods) { - return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY, + return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, Arrays.asList(periods)); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 6cc9397596..cbfd0a5951 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -67,6 +67,12 @@ public class DashManifest { */ public final long suggestedPresentationDelayMs; + /** + * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long publishTimeMs; + /** * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. @@ -82,8 +88,8 @@ public class DashManifest { public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, - long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, - List periods) { + long suggestedPresentationDelayMs, long publishTimeMs, UtcTimingElement utcTiming, + Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; this.minBufferTimeMs = minBufferTimeMs; @@ -91,6 +97,7 @@ public class DashManifest { this.minUpdatePeriodMs = minUpdatePeriodMs; this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; + this.publishTimeMs = publishTimeMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -145,8 +152,8 @@ public class DashManifest { } long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, - minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, copyPeriods); + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, publishTimeMs, + utcTiming, location, copyPeriods); } private static ArrayList copyAdaptationSets( 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 aa4c6b1e30..9c50c6cf30 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 @@ -115,6 +115,7 @@ public class DashManifestParser extends DefaultHandler ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET; long suggestedPresentationDelayMs = dynamic ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET; + long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET); UtcTimingElement utcTiming = null; Uri location = null; @@ -167,17 +168,17 @@ public class DashManifestParser extends DefaultHandler } return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected DashManifest buildMediaPresentationDescription(long availabilityStartTime, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdateTimeMs, - long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, UtcTimingElement utcTiming, - Uri location, List periods) { + long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, + UtcTimingElement utcTiming, Uri location, List periods) { return new DashManifest(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { From d5b79f3a43ec2b996c71736b4b31fa8c34b30a84 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 13:22:41 -0800 Subject: [PATCH 073/148] Update gradle wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176693785 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2623db66fc..9f9081a945 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0' + classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: From cf3ab9051df177335eacfe14076b5dfb08448801 Mon Sep 17 00:00:00 2001 From: simophin Date: Fri, 24 Nov 2017 17:27:35 +1300 Subject: [PATCH 074/148] Guard against out-of-range timestamp We've found that in our production environment, the AAC stream's timestamp exceeds the 33bit limit from time to time, when it happens, `peekId3PrivTimestamp` returns a value that is greater than `TimestampAdjuster.MAX_PTS_PLUS_ONE`, which causes a overflow in `TimestampAdjuster.adjustTsTimestamp` (overflow inside `ptsToUs`) after playing for a while . When the overflow happens, the start time of the stream becomes negative and the playback simply stucks at buffering forever. I fully understand that the 33bit is a spec requirement, thus I asked our stream provider to correct this mistake. But in the mean time, I'd also like ExoPlayer to handle this situation more error tolerance, as in other platforms (iOS, browsers) we see more tolerance behavior. --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 5ca8675dd9..83167c152f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong(); + return id3Data.readLong() & ((1L << 33) - 1L); } } } From b19512fb20e5f832066067480d9c9151e62ba964 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 02:20:35 -0800 Subject: [PATCH 075/148] Propagate the player error to ExoPlayerTestRunner In a test run where no exceptions were thrown on the main thread and the test did not time out, exceptions from onPlayerError were not correctly propagated to the test thread (handleException would be called with null). Fix ExoPlayerTestRunner.onPlayerError to propagate the actual exception from the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176825907 --- .../google/android/exoplayer2/testutil/ExoPlayerTestRunner.java | 2 +- 1 file changed, 1 insertion(+), 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 591e63dc5b..30e0214b62 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 @@ -517,7 +517,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPlayerError(ExoPlaybackException error) { - handleException(exception); + handleException(error); } @Override From 6c1f562230f87bd9882a069d2f1b92fd00d31e9a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 10:00:44 -0800 Subject: [PATCH 076/148] Switch from currentTimeMillis to elapsedRealtime currentTimeMillis is not guaranteed to be monotonic and elapsedRealtime is recommend for interval timing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176853118 --- .../google/android/exoplayer2/util/ConditionVariable.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 262d120af8..058a5d6dd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -60,18 +60,18 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until timeout milliseconds have passed. + * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. * * @param timeout The maximum time to wait in milliseconds. - * @return true If the condition was opened, false if the call returns because of the timeout. + * @return True if the condition was opened, false if the call returns because of the timeout. * @throws InterruptedException If the thread is interrupted. */ public synchronized boolean block(long timeout) throws InterruptedException { - long now = System.currentTimeMillis(); + long now = android.os.SystemClock.elapsedRealtime(); long end = now + timeout; while (!isOpen && now < end) { wait(end - now); - now = System.currentTimeMillis(); + now = android.os.SystemClock.elapsedRealtime(); } return isOpen; } From 86d91a59a0eb15d19952e01f09038d6f050874d0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:45:24 -0800 Subject: [PATCH 077/148] Remove race condition when stopping FakeExoPlayer. A message to stop the playback and to quit the playback thread was posted in release(). The stop message removed all other already queued messages which might include the second message to quit the thread. That led to infinite waiting in the release method because the playback thread never got the quit signal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997104 --- .../testutil/FakeSimpleExoPlayer.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 4a5beb0501..f6f56ead77 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 @@ -166,27 +166,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - releaseMedia(); - changePlaybackState(Player.STATE_IDLE); - } - }); + stop(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(); - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - playbackThread.quit(); - } - }); + stop(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -525,6 +511,20 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } + private void stop(boolean quitPlaybackThread) { + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + releaseMedia(); + changePlaybackState(Player.STATE_IDLE); + if (quitPlaybackThread) { + playbackThread.quit(); + } + } + }); + } + } } From ce557b11fcec3515fdd4315ab8deb5001e379f48 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:57:45 -0800 Subject: [PATCH 078/148] Add final to boolean used within Runnable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997767 --- .../google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f6f56ead77..d8f71535da 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 @@ -511,7 +511,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(boolean quitPlaybackThread) { + private void stop(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { From c1c892f5ec5546257d1da1b125fc4c3466daede2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 27 Nov 2017 06:29:47 -0800 Subject: [PATCH 079/148] Support undefined text track language when preferred is not available Also slightly improve language normalization/documentation. For this CL, it is assumed that null and "und" languages are different entities. Once we fully tackle language tag normalization, we can decide whether to normalize the "undefined" language. Issue:#2867 Issue:#2980 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177008509 --- RELEASENOTES.md | 3 + .../java/com/google/android/exoplayer2/C.java | 5 + .../trackselection/DefaultTrackSelector.java | 149 ++++++++++++------ .../google/android/exoplayer2/util/Util.java | 14 +- .../DefaultTrackSelectorTest.java | 64 ++++++++ 5 files changed, 180 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f772bc9f19..ae5bc0fb95 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,9 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 592589e221..6a35c0c5e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -424,6 +424,11 @@ public final class C { */ public static final int SELECTION_FLAG_AUTOSELECT = 4; + /** + * Represents an undetermined language as an ISO 639 alpha-3 language code. + */ + public static final String LANGUAGE_UNDETERMINED = "und"; + /** * Represents a streaming or other media type. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index c789caded4..0029cdbd31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicReference; * Parameters currentParameters = trackSelector.getParameters(); * // Generate new parameters to prefer German audio and impose a maximum video size constraint. * Parameters newParameters = currentParameters - * .withPreferredAudioLanguage("de") + * .withPreferredAudioLanguage("deu") * .withMaxVideoSize(1024, 768); * // Set the new parameters on the selector. * trackSelector.setParameters(newParameters);} @@ -81,17 +81,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * {@code null} selects the default track, or the first track if there's no default. */ public final String preferredAudioLanguage; // Text /** - * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * default track if there is one, or no track otherwise. */ public final String preferredTextLanguage; + /** + * Whether a text track with undetermined language should be selected if no track with + * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + */ + public final boolean selectUndeterminedTextLanguage; // Video /** @@ -150,6 +155,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

        *
      • No preferred audio language is set.
      • *
      • No preferred text language is set.
      • + *
      • Text tracks with undetermined language are not selected if no track with + * {@link #preferredTextLanguage} is available.
      • *
      • Lowest bitrate track selections are not forced.
      • *
      • Adaptation between different mime types is not allowed.
      • *
      • Non seamless adaptation is allowed.
      • @@ -161,13 +168,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
      */ public Parameters() { - this(null, null, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** * @param preferredAudioLanguage See {@link #preferredAudioLanguage} * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. * @param forceLowestBitrate See {@link #forceLowestBitrate}. * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} @@ -181,13 +189,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, - boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, + boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -209,10 +218,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -223,10 +233,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + /** + * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. + */ + public Parameters withSelectUndeterminedTextLanguageAsFallback( + boolean selectUndeterminedTextLanguage) { + if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -236,10 +262,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (forceLowestBitrate == this.forceLowestBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -249,10 +276,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -262,10 +290,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -275,10 +304,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -288,10 +318,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoBitrate == this.maxVideoBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -320,10 +351,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -334,10 +366,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -350,10 +383,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -880,17 +914,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, params.preferredTextLanguage)) { + boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); + if (preferredLanguageFound + || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { if (isDefault) { - trackScore = 6; + trackScore = 8; } else if (!isForced) { // Prefer non-forced to forced if a preferred text language has been specified. Where // both are provided the non-forced track will usually contain the forced subtitles as // a subset. - trackScore = 5; + trackScore = 6; } else { trackScore = 4; } + trackScore += preferredLanguageFound ? 1 : 0; } else if (isDefault) { trackScore = 3; } else if (isForced) { @@ -980,6 +1017,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} does not define a language. + * + * @param format The {@link Format}. + * @return Whether the {@link Format} does not define a language. + */ + protected static boolean formatHasNoLanguage(Format format) { + return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED); + } + /** * Returns whether a {@link Format} specifies a particular language, or {@code false} if * {@code language} is null. 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 5b2de1042e..24c5f4036d 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 @@ -50,6 +50,7 @@ import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -238,13 +239,18 @@ public final class Util { } /** - * Returns a normalized RFC 5646 language code. + * Returns a normalized RFC 639-2/T code for {@code language}. * - * @param language A possibly non-normalized RFC 5646 language code. - * @return The normalized code, or null if the input was null. + * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. + * @return The all-lowercase normalized code, or null if the input was null, or + * {@code language.toLowerCase()} if the language could not be normalized. */ public static String normalizeLanguageCode(String language) { - return language == null ? null : new Locale(language).getLanguage(); + try { + return language == null ? null : new Locale(language).getISO3Language(); + } catch (MissingResourceException e) { + return language.toLowerCase(); + } } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index a0e499139c..b2b149b004 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -36,6 +36,8 @@ public final class DefaultTrackSelectorTest { private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_TEXT); private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); @@ -534,6 +536,60 @@ public final class DefaultTrackSelectorTest { .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** + * Tests that the default track selector will select a text track with undetermined language if no + * text track with the preferred language is available but + * {@link Parameters#selectUndeterminedTextLanguage} is true. + */ + @Test + public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ + Format spanish = Format.createTextContainerFormat("spanish", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "spa"); + Format german = Format.createTextContainerFormat("german", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "de"); + Format undeterminedUnd = Format.createTextContainerFormat("undeterminedUnd", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "und"); + Format undeterminedNull = Format.createTextContainerFormat("undeterminedNull", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, null); + + RendererCapabilities[] textRendererCapabilites = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedNull); + + result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german)); + assertThat(result.selections.get(0)).isNull(); + } + /** * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} * indicate lowest bitrate preference, even when tracks are within capabilities. @@ -562,6 +618,14 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(new TrackGroup(formats)); } + private static TrackGroupArray wrapFormats(Format... formats) { + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < trackGroups.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + return new TrackGroupArray(trackGroups); + } + /** * A {@link RendererCapabilities} that advertises support for all formats of a given type using * a provided support value. For any format that does not have the given track type, From 1861aea2b3159537e11d7e5329210dede110e048 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 07:02:33 -0800 Subject: [PATCH 080/148] Add throws IllegalSeekPositionException doc to seekTo(windowIndex, positionMs). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177011497 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 2 ++ 1 file changed, 2 insertions(+) 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 dc703f924a..d911f83392 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 @@ -368,6 +368,8 @@ public interface Player { * @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. + * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided + * {@code windowIndex} is not within the bounds of the current timeline. */ void seekTo(int windowIndex, long positionMs); From 23a7f2d994007f59bd88106e35db45b93f78fcb5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 12:42:53 -0800 Subject: [PATCH 081/148] Force wrapping of HLS ID3 timestamp Merge of https://github.com/google/ExoPlayer/pull/3495 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177057183 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 83167c152f..1ad5acc5c5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong() & ((1L << 33) - 1L); + return id3Data.readLong() & 0x1FFFFFFFFL; } } } From d3fd2d1b872d2584453a60b5aaf3fe8ef502f48e Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 28 Nov 2017 17:02:04 +0000 Subject: [PATCH 082/148] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..e85c0c28c7 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From e175bf9e4271bae6c85bc49ff2d51897660de5c2 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:45:54 +0100 Subject: [PATCH 083/148] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index a82b5af583..edd941c1dd 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -934,19 +934,39 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { + private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - // TODO: It may be necessary to handle timestamp offsets from UTC. - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); + + if (firstLine != null) { + //determine format pattern + String formatPattern; + if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_2; + } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_3; + } else { + formatPattern = ISO_8601_FORMAT; + } + //parse + try { + SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.parse(firstLine).getTime(); + } catch (ParseException e) { + throw new ParserException(e); + } + + } else { + throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } } From ee05b60a19ef9bb1e43d9e7d337a41552f3f9f78 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:52:12 +0100 Subject: [PATCH 084/148] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index edd941c1dd..f1ee813020 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -935,10 +935,9 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { @@ -947,10 +946,10 @@ public final class DashMediaSource implements MediaSource { if (firstLine != null) { //determine format pattern String formatPattern; - if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_2; - } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_3; + if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; } else { formatPattern = ISO_8601_FORMAT; } @@ -967,6 +966,7 @@ public final class DashMediaSource implements MediaSource { throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } + } + } From 1d96492c1e370cf5c0792c987d106ea579285856 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 13:32:44 -0800 Subject: [PATCH 085/148] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177063576 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 1ad5acc5c5..c4e54d4bd3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -306,6 +306,8 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); + // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the + // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495. return id3Data.readLong() & 0x1FFFFFFFFL; } } From 7a031980281555edd99d353469a7fdc5ea7fec22 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 03:58:30 -0800 Subject: [PATCH 086/148] Add some clarifications to MediaSource documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177141094 --- .../android/exoplayer2/source/MediaSource.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 7288b39897..4a0d8e196d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -35,7 +35,8 @@ import java.io.IOException; * player to load and read the media. *
    * All methods are called on the player's internal playback thread, as described in the - * {@link ExoPlayer} Javadoc. + * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances + * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once. */ public interface MediaSource { @@ -150,6 +151,8 @@ public interface MediaSource { /** * Starts preparation of the source. + *

    + * Should not be called directly from application code. * * @param player The player for which this source is being prepared. * @param isTopLevelSource Whether this source has been passed directly to @@ -162,6 +165,8 @@ public interface MediaSource { /** * Throws any pending error encountered while loading or refreshing source information. + *

    + * Should not be called directly from application code. */ void maybeThrowSourceInfoRefreshError() throws IOException; @@ -169,6 +174,8 @@ public interface MediaSource { * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called * multiple times with the same period identifier without an intervening call to * {@link #releasePeriod(MediaPeriod)}. + *

    + * Should not be called directly from application code. * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -178,6 +185,8 @@ public interface MediaSource { /** * Releases the period. + *

    + * Should not be called directly from application code. * * @param mediaPeriod The period to release. */ @@ -186,8 +195,7 @@ public interface MediaSource { /** * Releases the source. *

    - * This method should be called when the source is no longer required. It may be called in any - * state. + * Should not be called directly from application code. */ void releaseSource(); From 0e0300b802bc10e210b0e36db18ecf35ecd30af2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 08:40:21 -0800 Subject: [PATCH 087/148] Extractor cleanup - Align class summary Javadoc - Fix ErrorProne + Style warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177165593 --- .../extractor/flv/FlvExtractor.java | 46 ++++++++++++------- .../extractor/mkv/MatroskaExtractor.java | 2 +- .../extractor/mp3/Mp3Extractor.java | 2 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 3 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../extractor/mp4/Mp4Extractor.java | 2 +- .../extractor/ogg/DefaultOggSeeker.java | 2 +- .../exoplayer2/extractor/ogg/FlacReader.java | 3 +- .../extractor/ogg/OggExtractor.java | 2 +- .../extractor/rawcc/RawCcExtractor.java | 2 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 2 +- .../extractor/ts/AdtsExtractor.java | 2 +- .../exoplayer2/extractor/ts/PsExtractor.java | 2 +- .../exoplayer2/extractor/ts/TsExtractor.java | 2 +- .../extractor/wav/WavExtractor.java | 4 +- 15 files changed, 47 insertions(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 218e6ffd82..30b66d65fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -25,9 +26,11 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** - * Facilitates the extraction of data from the FLV container format. + * Extracts data from the FLV container format. */ public final class FlvExtractor implements Extractor, SeekMap { @@ -43,16 +46,22 @@ public final class FlvExtractor implements Extractor, SeekMap { }; - // Header sizes. - private static final int FLV_HEADER_SIZE = 9; - private static final int FLV_TAG_HEADER_SIZE = 11; - - // Parser states. + /** + * Extractor states. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_FLV_HEADER, STATE_SKIPPING_TO_TAG_HEADER, STATE_READING_TAG_HEADER, + STATE_READING_TAG_DATA}) + private @interface States {} private static final int STATE_READING_FLV_HEADER = 1; private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; private static final int STATE_READING_TAG_HEADER = 3; private static final int STATE_READING_TAG_DATA = 4; + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + // Tag types. private static final int TAG_TYPE_AUDIO = 8; private static final int TAG_TYPE_VIDEO = 9; @@ -71,11 +80,11 @@ public final class FlvExtractor implements Extractor, SeekMap { private ExtractorOutput extractorOutput; // State variables. - private int parserState; + private @States int state; private int bytesToNextTagHeader; - public int tagType; - public int tagDataSize; - public long tagTimestampUs; + private int tagType; + private int tagDataSize; + private long tagTimestampUs; // Tags readers. private AudioTagPayloadReader audioReader; @@ -87,7 +96,7 @@ public final class FlvExtractor implements Extractor, SeekMap { headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; } @Override @@ -128,7 +137,7 @@ public final class FlvExtractor implements Extractor, SeekMap { @Override public void seek(long position, long timeUs) { - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; bytesToNextTagHeader = 0; } @@ -141,7 +150,7 @@ public final class FlvExtractor implements Extractor, SeekMap { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { while (true) { - switch (parserState) { + switch (state) { case STATE_READING_FLV_HEADER: if (!readFlvHeader(input)) { return RESULT_END_OF_INPUT; @@ -160,6 +169,9 @@ public final class FlvExtractor implements Extractor, SeekMap { return RESULT_CONTINUE; } break; + default: + // Never happens. + throw new IllegalStateException(); } } } @@ -199,7 +211,7 @@ public final class FlvExtractor implements Extractor, SeekMap { // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return true; } @@ -213,7 +225,7 @@ public final class FlvExtractor implements Extractor, SeekMap { private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { input.skipFully(bytesToNextTagHeader); bytesToNextTagHeader = 0; - parserState = STATE_READING_TAG_HEADER; + state = STATE_READING_TAG_HEADER; } /** @@ -236,7 +248,7 @@ public final class FlvExtractor implements Extractor, SeekMap { tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; tagHeaderBuffer.skipBytes(3); // streamId - parserState = STATE_READING_TAG_DATA; + state = STATE_READING_TAG_DATA; return true; } @@ -261,7 +273,7 @@ public final class FlvExtractor implements Extractor, SeekMap { wasConsumed = false; } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; } 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 5aefd041c4..4b0bbda275 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 @@ -53,7 +53,7 @@ import java.util.Locale; import java.util.UUID; /** - * Extracts data from a Matroska or WebM file. + * Extracts data from the Matroska and WebM container formats. */ public final class MatroskaExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index a4349ada09..dc7d21851a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -38,7 +38,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Extracts data from an MP3 file. + * Extracts data from the MP3 container format. */ public final class Mp3Extractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 5e8d72f18d..9b1158dfa8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -113,12 +113,13 @@ import com.google.android.exoplayer2.util.Util; fx = 256f; } else { int a = (int) percent; - float fa, fb; + float fa; if (a == 0) { fa = 0f; } else { fa = tableOfContents[a - 1]; } + float fb; if (a < 99) { fb = tableOfContents[a]; } else { 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 e86157dd92..4bc1b04418 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 @@ -53,7 +53,7 @@ import java.util.Stack; import java.util.UUID; /** - * Facilitates the extraction of data from the fragmented mp4 container format. + * Extracts data from the FMP4 container format. */ public final class FragmentedMp4Extractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index f23af98e7f..f2412bf4ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -41,7 +41,7 @@ import java.util.List; import java.util.Stack; /** - * Extracts data from an unfragmented MP4 file. + * Extracts data from the MP4 container format. */ public final class Mp4Extractor implements Extractor, SeekMap { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index 5470e2badc..77def57275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -186,7 +186,7 @@ import java.io.IOException; return start; } - long offset = pageSize * (granuleDistance <= 0 ? 2 : 1); + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); long nextPosition = input.getPosition() - offset + (granuleDistance * (end - start) / (endGranule - startGranule)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index f4da6e3960..304fb3dd96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -118,8 +118,9 @@ import java.util.List; case 14: case 15: return 256 << (blockSizeCode - 8); + default: + return -1; } - return -1; } private class FlacOggSeeker implements OggSeeker, SeekMap { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 54e168c665..a4d8f97d5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; /** - * Ogg {@link Extractor}. + * Extracts data from the Ogg container format. */ public class OggExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index 7840eafce6..aa77aba30e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts CEA data from a RawCC file. + * Extracts data from the RawCC container format. */ public final class RawCcExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 4d54600c6d..bc37277c57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from (E-)AC-3 bitstreams. + * Extracts data from (E-)AC-3 bitstreams. */ public final class Ac3Extractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 5ce15952a5..a0a748660e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from AAC bit streams with ADTS framing. + * Extracts data from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index 69c5745eaa..f3aad6ba6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 PS container format. + * Extracts data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 213d30d47d..13e669da23 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -45,7 +45,7 @@ import java.util.Collections; import java.util.List; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Extracts data from the MPEG-2 TS container format. */ public final class TsExtractor implements Extractor { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index cb46aa5519..cb9a2653d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -28,7 +28,9 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; -/** {@link Extractor} to extract samples from a WAV byte stream. */ +/** + * Extracts data from WAV byte streams. + */ public final class WavExtractor implements Extractor, SeekMap { /** From a8d4ad94f4011ec310e3d3359d06d405bd9ef37e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 09:22:32 -0800 Subject: [PATCH 088/148] Fix DefaultTrackSelector#Parameter withSelectUndeterminedTextLanguage ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177170994 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 3 +-- .../exoplayer2/trackselection/DefaultTrackSelectorTest.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 0029cdbd31..49b8e8964b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -243,8 +243,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. */ - public Parameters withSelectUndeterminedTextLanguageAsFallback( - boolean selectUndeterminedTextLanguage) { + public Parameters withSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { return this; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index b2b149b004..6b14d139ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -561,8 +561,7 @@ public final class DefaultTrackSelectorTest { wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters( - DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.setParameters(DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); @@ -577,7 +576,7 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0)).isNull(); trackSelector.setParameters( - trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.getParameters().withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); From 9eed1150e083a224a732506ed9db66bd3699f989 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 09:43:08 -0800 Subject: [PATCH 089/148] Clean up some extrator SeekMap implementations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177173618 --- .../extractor/flv/FlvExtractor.java | 45 +++++------ .../extractor/flv/ScriptTagPayloadReader.java | 8 +- .../extractor/wav/WavExtractor.java | 21 +---- .../exoplayer2/extractor/wav/WavHeader.java | 76 ++++++++++++------- 4 files changed, 69 insertions(+), 81 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 30b66d65fd..2da075ff53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -32,7 +32,7 @@ import java.lang.annotation.RetentionPolicy; /** * Extracts data from the FLV container format. */ -public final class FlvExtractor implements Extractor, SeekMap { +public final class FlvExtractor implements Extractor { /** * Factory for {@link FlvExtractor} instances. @@ -70,32 +70,28 @@ public final class FlvExtractor implements Extractor, SeekMap { // FLV container identifier. private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); - // Temporary buffers. private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagData; + private final ScriptTagPayloadReader metadataReader; - // Extractor outputs. private ExtractorOutput extractorOutput; - - // State variables. private @States int state; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; private long tagTimestampUs; - - // Tags readers. + private boolean outputSeekMap; private AudioTagPayloadReader audioReader; private VideoTagPayloadReader videoReader; - private ScriptTagPayloadReader metadataReader; public FlvExtractor() { scratch = new ParsableByteArray(4); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); + metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; } @@ -203,11 +199,7 @@ public final class FlvExtractor implements Extractor, SeekMap { videoReader = new VideoTagPayloadReader( extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } - if (metadataReader == null) { - metadataReader = new ScriptTagPayloadReader(null); - } extractorOutput.endTracks(); - extractorOutput.seekMap(this); // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; @@ -263,11 +255,18 @@ public final class FlvExtractor implements Extractor, SeekMap { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { + ensureOutputSeekMap(); audioReader.consume(prepareTagData(input), tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { + ensureOutputSeekMap(); videoReader.consume(prepareTagData(input), tagTimestampUs); - } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { + } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); + long durationUs = metadataReader.getDurationUs(); + if (durationUs != C.TIME_UNSET) { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + outputSeekMap = true; + } } else { input.skipFully(tagDataSize); wasConsumed = false; @@ -289,21 +288,11 @@ public final class FlvExtractor implements Extractor, SeekMap { return tagData; } - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return false; - } - - @Override - public long getDurationUs() { - return metadataReader.getDurationUs(); - } - - @Override - public long getPosition(long timeUs) { - return 0; + private void ensureOutputSeekMap() { + if (!outputSeekMap) { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + outputSeekMap = true; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 1a4f8f3e88..2dec85ffcc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,11 +43,8 @@ import java.util.Map; private long durationUs; - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public ScriptTagPayloadReader(TrackOutput output) { - super(output); + public ScriptTagPayloadReader() { + super(null); durationUs = C.TIME_UNSET; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index cb9a2653d7..4f2be71a69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; @@ -31,7 +30,7 @@ import java.io.IOException; /** * Extracts data from WAV byte streams. */ -public final class WavExtractor implements Extractor, SeekMap { +public final class WavExtractor implements Extractor { /** * Factory for {@link WavExtractor} instances. @@ -95,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(this); + extractorOutput.seekMap(wavHeader); } int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); @@ -115,20 +114,4 @@ public final class WavExtractor implements Extractor, SeekMap { return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } - // SeekMap implementation. - - @Override - public long getDurationUs() { - return wavHeader.getDurationUs(); - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - return wavHeader.getPosition(timeUs); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index a57060f604..1c1fc97a22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -16,9 +16,10 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; /** Header for a WAV file. */ -/*package*/ final class WavHeader { +/* package */ final class WavHeader implements SeekMap { /** Number of audio chanels. */ private final int numChannels; @@ -49,12 +50,56 @@ import com.google.android.exoplayer2.C; this.encoding = encoding; } - /** Returns the duration in microseconds of this WAV. */ + // Setting bounds. + + /** + * Sets the data start position and size in bytes of sample data in this WAV. + * + * @param dataStartPosition The data start position in bytes. + * @param dataSize The data size in bytes. + */ + public void setDataBounds(long dataStartPosition, long dataSize) { + this.dataStartPosition = dataStartPosition; + this.dataSize = dataSize; + } + + /** Returns whether the data start position and size have been set. */ + public boolean hasDataBounds() { + return dataStartPosition != 0 && dataSize != 0; + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override public long getDurationUs() { long numFrames = dataSize / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } + @Override + public long getPosition(long timeUs) { + long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Round down to nearest frame. + long position = (unroundedPosition / blockAlignment) * blockAlignment; + return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + } + + // Misc getters. + + /** + * Returns the time in microseconds for the given position in bytes. + * + * @param position The position in bytes. + */ + public long getTimeUs(long position) { + return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + } + /** Returns the bytes per frame of this WAV. */ public int getBytesPerFrame() { return blockAlignment; @@ -75,33 +120,8 @@ import com.google.android.exoplayer2.C; return numChannels; } - /** Returns the position in bytes in this WAV for the given time in microseconds. */ - public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; - } - - /** Returns the time in microseconds for the given position in bytes in this WAV. */ - public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; - } - - /** Returns true if the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; - } - - /** Sets the start position and size in bytes of sample data in this WAV. */ - public void setDataBounds(long dataStartPosition, long dataSize) { - this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; - } - /** Returns the PCM encoding. **/ - @C.PcmEncoding - public int getEncoding() { + public @C.PcmEncoding int getEncoding() { return encoding; } From c12349e2c937ed6bd9fcea6b9ac47b9031f62bda Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Nov 2017 09:53:30 -0800 Subject: [PATCH 090/148] Allow setting supported formats on AdsLoaders ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177175377 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 24 +++++++++++++++++++ .../exoplayer2/source/ads/AdsLoader.java | 10 ++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 1 + .../android/exoplayer2/util/MimeTypes.java | 3 +++ .../source/dash/DashMediaSource.java | 3 +-- 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 5b61db0264..cf8b8a3f6d 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 @@ -49,10 +49,13 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -117,6 +120,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private List supportedMimeTypes; private EventListener eventListener; private Player player; private ViewGroup adUiViewGroup; @@ -238,6 +242,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // AdsLoader implementation. + @Override + public void setSupportedContentTypes(@C.ContentType int... contentTypes) { + List supportedMimeTypes = new ArrayList<>(); + for (@C.ContentType int contentType : contentTypes) { + if (contentType == C.TYPE_DASH) { + supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); + } else if (contentType == C.TYPE_HLS) { + supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); + } else if (contentType == C.TYPE_OTHER) { + supportedMimeTypes.addAll(Arrays.asList( + MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG, + MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); + } else if (contentType == C.TYPE_SS) { + // IMA does not support SmoothStreaming ad media. + } + } + this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); + } + @Override public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; @@ -296,6 +319,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(true); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with preloading"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 241750a21f..99feccd2f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.ads; import android.view.ViewGroup; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import java.io.IOException; @@ -71,6 +72,15 @@ public interface AdsLoader { } + /** + * Sets the supported content types for ad media. Must be called before the first call to + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored. + * + * @param contentTypes The supported content types for ad media. Each element must be one of + * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}. + */ + void setSupportedContentTypes(@C.ContentType int... contentTypes); + /** * Attaches a player that will play ads loaded using this instance. Called on the main thread by * {@link AdsMediaSource}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 397b8effd3..202e31cba1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -132,6 +132,7 @@ public final class AdsMediaSource implements MediaSource { period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; + adsLoader.setSupportedContentTypes(C.TYPE_OTHER); } @Override 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 a68e0142d6..8307e998a0 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 @@ -36,6 +36,7 @@ public final class MimeTypes { public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @@ -70,7 +71,9 @@ public final class MimeTypes { public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index f1ee813020..c5fbafb84e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -877,8 +877,7 @@ public final class DashMediaSource implements MediaSource { } - private final class ManifestCallback implements - Loader.Callback> { + private final class ManifestCallback implements Loader.Callback> { @Override public void onLoadCompleted(ParsingLoadable loadable, From 63dbf56b6c0bcd69d8e218a7aa55e3214c94b130 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 10:45:38 -0800 Subject: [PATCH 091/148] Allow multiple video and audio debug listeners in SimpleExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177184331 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../android/exoplayer2/SimpleExoPlayer.java | 82 +++++++++++++++---- .../exoplayer2/testutil/ExoHostedTest.java | 32 +++++++- 3 files changed, 97 insertions(+), 21 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 efde775176..cf0f8b8dc8 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 @@ -298,8 +298,8 @@ public class PlayerActivity extends Activity implements OnClickListener, player.addListener(new PlayerEventListener()); player.addListener(eventLogger); player.addMetadataOutput(eventLogger); - player.setAudioDebugListener(eventLogger); - player.setVideoDebugListener(eventLogger); + player.addAudioDebugListener(eventLogger); + player.addVideoDebugListener(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); 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 5a5a948d58..1374b73709 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 @@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet videoDebugListeners; + private final CopyOnWriteArraySet audioDebugListeners; private final int videoRendererCount; private final int audioRendererCount; @@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private AudioRendererEventListener audioDebugListener; - private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; @@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer { videoListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -576,18 +578,64 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive debug events from the video renderer. * * @param listener The listener. + * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}. */ + @Deprecated public void setVideoDebugListener(VideoRendererEventListener listener) { - videoDebugListener = listener; + videoDebugListeners.clear(); + if (listener != null) { + addVideoDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void addVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void removeVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.remove(listener); } /** * Sets a listener to receive debug events from the audio renderer. * * @param listener The listener. + * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}. */ + @Deprecated public void setAudioDebugListener(AudioRendererEventListener listener) { - audioDebugListener = listener; + audioDebugListeners.clear(); + if (listener != null) { + addAudioDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void addAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void removeAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.remove(listener); } // ExoPlayer implementation @@ -877,7 +925,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoEnabled(DecoderCounters counters) { videoDecoderCounters = counters; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoEnabled(counters); } } @@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -894,14 +942,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoInputFormatChanged(Format format) { videoFormat = format; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoInputFormatChanged(format); } } @Override public void onDroppedFrames(int count, long elapsed) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onDroppedFrames(count, elapsed); } } @@ -913,7 +961,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -926,14 +974,14 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onRenderedFirstFrame(); } } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onRenderedFirstFrame(surface); } } @Override public void onVideoDisabled(DecoderCounters counters) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDisabled(counters); } videoFormat = null; @@ -945,7 +993,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioEnabled(DecoderCounters counters) { audioDecoderCounters = counters; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioEnabled(counters); } } @@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSessionId(int sessionId) { audioSessionId = sessionId; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } } @@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -970,7 +1018,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioInputFormatChanged(Format format) { audioFormat = format; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioInputFormatChanged(format); } } @@ -978,14 +1026,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } @Override public void onAudioDisabled(DecoderCounters counters) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDisabled(counters); } audioFormat = null; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ee4018ba0e..5ff0533f71 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -78,6 +78,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private Surface surface; private ExoPlaybackException playerError; private Player.EventListener playerEventListener; + private VideoRendererEventListener videoDebugListener; + private AudioRendererEventListener audioDebugListener; private boolean playerWasPrepared; private boolean playing; @@ -140,6 +142,26 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen } } + /** + * Sets an {@link VideoRendererEventListener} to listen for video debug events during the test. + */ + public final void setVideoDebugListener(VideoRendererEventListener videoDebugListener) { + this.videoDebugListener = videoDebugListener; + if (player != null) { + player.addVideoDebugListener(videoDebugListener); + } + } + + /** + * Sets an {@link AudioRendererEventListener} to listen for audio debug events during the test. + */ + public final void setAudioDebugListener(AudioRendererEventListener audioDebugListener) { + this.audioDebugListener = audioDebugListener; + if (player != null) { + player.addAudioDebugListener(audioDebugListener); + } + } + // HostedTest implementation @Override @@ -155,9 +177,15 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen if (playerEventListener != null) { player.addListener(playerEventListener); } + if (videoDebugListener != null) { + player.addVideoDebugListener(videoDebugListener); + } + if (audioDebugListener != null) { + player.addAudioDebugListener(audioDebugListener); + } player.addListener(this); - player.setAudioDebugListener(this); - player.setVideoDebugListener(this); + player.addAudioDebugListener(this); + player.addVideoDebugListener(this); player.setPlayWhenReady(true); actionHandler = new Handler(); // Schedule any pending actions. From 91b324f6b970c8b036289872e03841ba1754ab64 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 08:53:27 -0800 Subject: [PATCH 092/148] Fix weird XingSeeker indexing There are still things broken about the seeker, but this cleans up some of the weird bits. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177315136 --- .../exoplayer2/extractor/mp3/XingSeeker.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 9b1158dfa8..55888066e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -58,9 +58,8 @@ import com.google.android.exoplayer2.util.Util; } long sizeBytes = frame.readUnsignedIntToInt(); - frame.skipBytes(1); - long[] tableOfContents = new long[99]; - for (int i = 0; i < 99; i++) { + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); } @@ -105,30 +104,20 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable()) { return firstFramePosition; } - float percent = timeUs * 100f / durationUs; - float fx; - if (percent <= 0f) { - fx = 0f; - } else if (percent >= 100f) { - fx = 256f; + double percent = (timeUs * 100d) / durationUs; + double fx; + if (percent <= 0) { + fx = 0; + } else if (percent >= 100) { + fx = 256; } else { int a = (int) percent; - float fa; - if (a == 0) { - fa = 0f; - } else { - fa = tableOfContents[a - 1]; - } - float fb; - if (a < 99) { - fb = tableOfContents[a]; - } else { - fb = 256f; - } + float fa = tableOfContents[a]; + float fb = a == 99 ? 256 : tableOfContents[a + 1]; fx = fa + (fb - fa) * (percent - a); } - long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; + long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 : firstFramePosition - headerSize + sizeBytes - 1; return Math.min(position, maximumPosition); @@ -139,14 +128,14 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable() || position < firstFramePosition) { return 0L; } - double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; + double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; + Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); long previousTime = getTimeUsForTocPosition(previousTocPosition); // Linearly interpolate the time taking into account the next entry. - long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; + long previousByte = tableOfContents[previousTocPosition]; + long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte)); @@ -163,7 +152,7 @@ import com.google.android.exoplayer2.util.Util; * interpreted as a percentage of the stream's duration between 0 and 100. */ private long getTimeUsForTocPosition(int tocPosition) { - return durationUs * tocPosition / 100; + return (durationUs * tocPosition) / 100; } } From 800cfeea3d18d2f7d1e8198c2a483460413fa08f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 09:09:17 -0800 Subject: [PATCH 093/148] Optimize seeking for unseekable SeekMaps - Avoid re-downloading data prior to the first mdat box when seeking back to the start of an unseekable FMP4. - Avoid re-downloading data prior to the first frame for constant bitrate MP3. - Update SeekMap.getPosition documentation to allow a non-zero position for the unseekable case. Note that XingSeeker was already returning a non-zero position if unseekable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177317256 --- .../android/exoplayer2/extractor/SeekMap.java | 16 ++++++++++++++-- .../extractor/mp3/ConstantBitrateSeeker.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 3 ++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 778aa4d715..964c43a45a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -28,13 +28,24 @@ public interface SeekMap { final class Unseekable implements SeekMap { private final long durationUs; + private final long startPosition; /** * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if * the duration is unknown. */ public Unseekable(long durationUs) { + this(durationUs, 0); + } + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if + * the duration is unknown. + * @param startPosition The position (byte offset) of the start of the media. + */ + public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; + this.startPosition = startPosition; } @Override @@ -49,7 +60,7 @@ public interface SeekMap { @Override public long getPosition(long timeUs) { - return 0; + return startPosition; } } @@ -78,7 +89,8 @@ public interface SeekMap { * * @param timeUs A seek position in microseconds. * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor, or 0 if {@code #isSeekable()} returns false. + * to the extractor. If {@link #isSeekable()} returns false then the returned value will be + * independent of {@code timeUs}, and will indicate the start of the media in the stream. */ long getPosition(long timeUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index df7748a910..47e12161a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (durationUs == C.TIME_UNSET) { - return 0; + return firstFramePosition; } timeUs = Util.constrainValue(timeUs, 0, durationUs); return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); 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 4bc1b04418..28a1ffaa7b 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 @@ -345,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor { currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { - extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); haveOutputSeekMap = true; } parserState = STATE_READING_ENCRYPTION_DATA; From f0f726dfa9a6cdcb01a33d6ed24140fbc471ab44 Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Wed, 29 Nov 2017 16:59:41 -0800 Subject: [PATCH 094/148] Add manifestless captions support. This code fits into the pre-existing captions fetcher architecture. 1. ManifestlessCaptionsMetadata Other captions fetchers must first fetch a manifest (HLS or manifest) to discover captions tracks. This process does not exist for manifestless. All we need to do is scan the FormatStream's for the right itag, so this is an all-static class. 2. ManifestlessSubtitleWindowProvider Once a captions track is selected, a subtitles provider is instantiated. This is the main interface used by the player to retrieve captions according to playback position. This class stores fetched captions in a tree index by time for efficient lookups. Background captions fetches are used to populate the tree. 3. ManifestlessCaptionsFetch Captions are fetched one segment at a time. One instance of this object is required per fetch. It performs a blocking fetch on call(), and is intended to be submitted to a background-thread executor. 4. ManifestlessCaptionsFetch.CaptionSegment This is the result of the caption fetch. These values are used to populate the captions tree. Manifestlessness The initial request is always a headm request. There is a separate tree of every segment indexed by start time. This tree is used to improve manifestless sequence number calculation. Once we have data for the current timestamp, we walk forward through the tree to find the next unfetched sequence number, and fetch that. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177385094 --- .../google/android/exoplayer2/text/webvtt/WebvttCssStyle.java | 4 +++- .../com/google/android/exoplayer2/text/webvtt/WebvttCue.java | 4 ++-- .../android/exoplayer2/text/webvtt/WebvttCueParser.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 10c17e2888..a78c5afa78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -31,10 +31,11 @@ import java.util.List; * @see W3C specification - Apply * CSS properties */ -/* package */ final class WebvttCssStyle { +public final class WebvttCssStyle { public static final int UNSPECIFIED = -1; + /** Style flag enum */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) @@ -44,6 +45,7 @@ import java.util.List; public static final int STYLE_ITALIC = Typeface.ITALIC; public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + /** Font size unit enum */ @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) public @interface FontSizeUnit {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index 295fdc656f..e16b231f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.text.Cue; /** * A representation of a WebVTT cue. */ -/* package */ final class WebvttCue extends Cue { +public final class WebvttCue extends Cue { public final long startTime; public final long endTime; @@ -59,7 +59,7 @@ import com.google.android.exoplayer2.text.Cue; * Builder for WebVTT cues. */ @SuppressWarnings("hiding") - public static final class Builder { + public static class Builder { private static final String TAG = "WebvttCueBuilder"; 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 54af4dbf63..80ebecdc0e 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 @@ -45,7 +45,7 @@ import java.util.regex.Pattern; /** * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ -/* package */ final class WebvttCueParser { +public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); @@ -90,7 +90,7 @@ import java.util.regex.Pattern; * @param styles List of styles defined by the CSS style blocks preceeding the cues. * @return Whether a valid Cue was found. */ - /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, + public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); if (firstLine == null) { From 2567bf51fc10ee8466d4aefbe062f5c01e5add15 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 00:33:10 -0800 Subject: [PATCH 095/148] Log load errors from AdsMediaSource in the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177419981 --- .../android/exoplayer2/demo/EventLogger.java | 21 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 3 ++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 9233b016f5..4819c28753 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -57,7 +58,8 @@ import java.util.Locale; */ /* package */ final class EventLogger implements Player.EventListener, MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { + ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -369,6 +371,23 @@ import java.util.Locale; // Do nothing. } + // AdsMediaSource.EventListener + + @Override + public void onAdLoadError(IOException error) { + printInternalError("loadError", error); + } + + @Override + public void onAdClicked() { + // Do nothing. + } + + @Override + public void onAdTapped() { + // Do nothing. + } + // Internal methods private void printInternalError(String type, Exception e) { 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 cf0f8b8dc8..7d0975a750 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 @@ -471,7 +471,8 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup); + return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, + mainHandler, eventLogger); } private void releaseAdsLoader() { From 9526e8586adedc8022abf1b4b30883efe3c69ea5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:25:28 -0800 Subject: [PATCH 096/148] Use a MediaSource factory internally in AdsMediaSource Support ad MediaSources that aren't prepared immediately by using DeferredMediaPeriod, moved up from DynamicConcatenatingMediaSource. In a later change the new interfaces will be made public so that apps can provide their own MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424172 --- .../source/DeferredMediaPeriod.java | 139 +++++++++++++++++ .../DynamicConcatenatingMediaSource.java | 108 ------------- .../exoplayer2/source/ads/AdsMediaSource.java | 143 +++++++++++++++--- 3 files changed, 259 insertions(+), 131 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java new file mode 100644 index 0000000000..bc29b2fdf1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.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; + +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; + +/** + * Media period that wraps a media source and defers calling its + * {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()} + * has been called. This is useful if you need to return a media period immediately but the media + * source that should create it is not yet prepared. + */ +public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + /** + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then + * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} + * to release the period. + */ + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + /** + * Releases the period. + */ + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + // MediaPeriod.Callback implementation + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + +} 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 e80abad3ef..b66e5ebe09 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 @@ -27,7 +27,6 @@ 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.DefaultShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -758,111 +757,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } - /** - * Media period used for periods created from unprepared media sources exposed through - * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes - * available. - */ - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - public final MediaSource mediaSource; - - private final MediaPeriodId id; - private final Allocator allocator; - - private MediaPeriod mediaPeriod; - private Callback callback; - private long preparePositionUs; - - public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { - this.id = id; - this.allocator = allocator; - this.mediaSource = mediaSource; - } - - public void createPeriod() { - mediaPeriod = mediaSource.createPeriod(id, allocator); - if (callback != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - public void releasePeriod() { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - } - } - - @Override - public void prepare(Callback callback, long preparePositionUs) { - this.callback = callback; - this.preparePositionUs = preparePositionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } else { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs) { - mediaPeriod.discardBuffer(positionUs); - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); - } - } - } - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 202e31cba1..47a2540c38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.ads; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; @@ -23,15 +24,19 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -68,12 +73,12 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; private final Handler mainHandler; private final ComponentListener componentListener; - private final Map adMediaSourceByMediaPeriod; + private final AdMediaSourceFactory adMediaSourceFactory; + private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @Nullable private final Handler eventHandler; @@ -95,6 +100,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

    + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -109,6 +117,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

    + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -121,18 +132,18 @@ public final class AdsMediaSource implements MediaSource { AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable AdsListener eventListener) { this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); + adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - adsLoader.setSupportedContentTypes(C.TYPE_OTHER); + adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @Override @@ -173,10 +184,9 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource.Builder( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) - .setEventListener(mainHandler, componentListener) - .build(); + Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; + final MediaSource adMediaSource = + adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -186,30 +196,37 @@ public final class AdsMediaSource implements MediaSource { Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { + deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList()); + adMediaSource.prepareSource(player, false, new MediaSource.Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + @Nullable Object manifest) { + onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline); } }); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + if (mediaPeriods == null) { + deferredMediaPeriod.createPeriod(); + } else { + // Keep track of the deferred media period so it can be populated with the real media period + // when the source's info becomes available. + mediaPeriods.add(deferredMediaPeriod); + } + return deferredMediaPeriod; } else { - return contentMediaSource.createPeriod(id, allocator); + DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + mediaPeriod.createPeriod(); + return mediaPeriod; } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); } @Override @@ -264,9 +281,17 @@ public final class AdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, + int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) { + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + for (int i = 0; i < mediaPeriods.size(); i++) { + mediaPeriods.get(i).createPeriod(); + } + deferredMediaPeriodByAdMediaSource.remove(mediaSource); + } maybeUpdateSourceInfo(); } @@ -285,7 +310,7 @@ public final class AdsMediaSource implements MediaSource { * Listener for component events. All methods are called on the main thread. */ private final class ComponentListener implements AdsLoader.EventListener, - ExtractorMediaSource.EventListener { + AdMediaSourceLoadErrorListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -349,4 +374,76 @@ public final class AdsMediaSource implements MediaSource { } + /** + * Listener for errors while loading an ad {@link MediaSource}. + */ + private interface AdMediaSourceLoadErrorListener { + + /** + * Called when an error occurs loading media data. + * + * @param error The load error. + */ + void onLoadError(IOException error); + + } + + /** + * Factory for {@link MediaSource}s for loading ad media. + */ + private interface AdMediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the ad. + * @param handler A handler for listener events. + * @param listener A listener for ad load errors. To have ad media source load errors notified + * via the ads media source's listener, call this listener's onLoadError method from your + * new media source's load error listener using the specified {@code handler}. Otherwise, + * this parameter can be ignored. + * @return The new media source. + */ + MediaSource createAdMediaSource(Uri uri, Handler handler, + AdMediaSourceLoadErrorListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or + * {@link C#TYPE_OTHER}. + * + * @return The content types supported by the factory. + */ + int[] getSupportedTypes(); + + } + + private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public MediaSource createAdMediaSource(Uri uri, Handler handler, + final AdMediaSourceLoadErrorListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, + new EventListener() { + @Override + public void onLoadError(IOException error) { + listener.onLoadError(error); + } + }).build(); + } + + @Override + public int[] getSupportedTypes() { + // Only ExtractorMediaSource is supported. + return new int[] {C.TYPE_OTHER}; + } + + } + } From 315a6c3558024038f3ca736844cb67534ab2806f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:27:07 -0800 Subject: [PATCH 097/148] Update getPosition(0) positions for FragmentedMp4Extractor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424314 --- .../src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump | 2 +- .../src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index bf822d9db4..95f6528fd6 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index 9d3755b23b..ebd33133e2 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 3 track 0: format: From 4e8b9282f518ea2568290c1a9b8655b0b8ca1d92 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 02:57:07 -0800 Subject: [PATCH 098/148] Add a notice that NDK <= version 15c is required for VP9 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177430827 --- extensions/vp9/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 941b413c09..649e4a6ee2 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` -* Download the [Android NDK][] and set its location in an environment variable: +* Download the [Android NDK][] and set its location in an environment variable. +Only versions up to NDK 15c are supported currently (see [#3520][]). ``` NDK_PATH="" @@ -70,6 +71,7 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#3520]: https://github.com/google/ExoPlayer/issues/3520 ## Notes ## From 8fbc2a5c9b0f9904eef67fb77792e3b42cf904b3 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 05:34:51 -0800 Subject: [PATCH 099/148] Snap to frame boundary in ConstantBitrateSeeker - This change snaps the seek position for constant bitrate MP3s to the nearest frame boundary, avoiding the need to skip one byte at a time to re-synchronize (this may still happen if the MP3 does not really have fixed size frames). - Tweaked both ConstantBitrateSeeker and WavHeader to ensure the returned positions are valid. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177441798 --- .../assets/mp3/play-trimmed.mp3.1.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.2.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.3.dump | 6 +++- .../extractor/mp3/ConstantBitrateSeeker.java | 32 +++++++++++++++---- .../extractor/mp3/Mp3Extractor.java | 4 +-- .../exoplayer2/extractor/wav/WavHeader.java | 11 ++++--- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index 47e12161a8..e02e99e139 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -26,27 +26,47 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; + private final long dataSize; + private final int frameSize; private final int bitrate; private final long durationUs; - public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) { + /** + * @param firstFramePosition The position (byte offset) of the first frame. + * @param inputLength The length of the stream. + * @param frameSize The size of a single frame in the stream. + * @param bitrate The stream's bitrate. + */ + public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, + int bitrate) { this.firstFramePosition = firstFramePosition; + this.frameSize = frameSize; this.bitrate = bitrate; - durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength); + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFramePosition; + durationUs = getTimeUs(inputLength); + } } @Override public boolean isSeekable() { - return durationUs != C.TIME_UNSET; + return dataSize != C.LENGTH_UNSET; } @Override public long getPosition(long timeUs) { - if (durationUs == C.TIME_UNSET) { + if (dataSize == C.LENGTH_UNSET) { return firstFramePosition; } - timeUs = Util.constrainValue(timeUs, 0, durationUs); - return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); + // Add data start position. + return firstFramePosition + positionOffset; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index dc7d21851a..7c579504c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -393,8 +393,8 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, - input.getLength()); + return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), + synchronizedHeader.frameSize, synchronizedHeader.bitrate); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index 1c1fc97a22..2cdd31cb6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ /* package */ final class WavHeader implements SeekMap { @@ -83,10 +84,12 @@ import com.google.android.exoplayer2.extractor.SeekMap; @Override public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / blockAlignment) * blockAlignment; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); + // Add data start position. + return dataStartPosition + positionOffset; } // Misc getters. From 022b85a625468235075de679cc19374fca823d8a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 05:39:50 -0800 Subject: [PATCH 100/148] Move resetting audio processors to initialize() The set of active audio processors was only updated on reconfiguration and when draining playback parameters completed. Draining playback parameters are cleared in reset(), so if parameters were set while paused then the sink was quickly reset, without draining completing, the set of active audio processors wouldn't be updated. This means that a switch to or from speed or pitch = 1 would not be handled correctly if made while paused and followed by a seek. Move resetting active audio processors from configure (where if the active audio processors were reset we'd always initialize a new AudioTrack) to initialize(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177442098 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ae5bc0fb95..a123c78323 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Fix handling of playback parameters changes while paused when followed by a + seek. ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ba62ac126e..eb27c0fe55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -364,9 +364,6 @@ public final class DefaultAudioSink implements AudioSink { encoding = audioProcessor.getOutputEncoding(); } } - if (flush) { - resetAudioProcessors(); - } } int channelConfig; @@ -492,6 +489,9 @@ public final class DefaultAudioSink implements AudioSink { // The old playback parameters may no longer be applicable so try to reset them now. setPlaybackParameters(playbackParameters); + // Flush and reset active audio processors. + resetAudioProcessors(); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { From 0ea8c8bfa0db99fb2322e5ed2b3c9fb66c2bf9f7 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 07:20:45 -0800 Subject: [PATCH 101/148] Fix VBRI and XING seekers - Remove skipping of the VBRI/XING frame before calculating position offsets. This was incorrect. Instead, a constraint is used to ensure we don't return positions within these frames, the difference being that the constraint adjusts only positions that would fall within the frames, where-as the previous approach shifted positions through the whole stream. - Excluded last entry in the VBRI table because it has an invalid position (the length of the stream). - Give variables in XingSeeker descriptive names. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177451295 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 308 +++++++++--------- .../androidTest/assets/mp3/bear.mp3.2.dump | 76 ++--- .../extractor/mp3/ConstantBitrateSeeker.java | 18 +- .../extractor/mp3/Mp3Extractor.java | 7 +- .../exoplayer2/extractor/mp3/VbriSeeker.java | 33 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 112 ++++--- .../extractor/mp3/XingSeekerTest.java | 26 +- 7 files changed, 298 insertions(+), 282 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 2e0b21050c..7b6fe9db37 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -25,309 +25,313 @@ track 0: language = null drmInitData = - initializationData: - sample count = 76 + sample count = 77 sample 0: - time = 945782 + time = 928567 + flags = 1 + data = length 384, hash F7E344F4 + sample 1: + time = 952567 flags = 1 data = length 384, hash 14EF6AFD - sample 1: - time = 969782 + sample 2: + time = 976567 flags = 1 data = length 384, hash 61C9B92C - sample 2: - time = 993782 + sample 3: + time = 1000567 flags = 1 data = length 384, hash ABE1368 - sample 3: - time = 1017782 + sample 4: + time = 1024567 flags = 1 data = length 384, hash 6A3B8547 - sample 4: - time = 1041782 + sample 5: + time = 1048567 flags = 1 data = length 384, hash 30E905FA - sample 5: - time = 1065782 + sample 6: + time = 1072567 flags = 1 data = length 384, hash 21A267CD - sample 6: - time = 1089782 + sample 7: + time = 1096567 flags = 1 data = length 384, hash D96A2651 - sample 7: - time = 1113782 + sample 8: + time = 1120567 flags = 1 data = length 384, hash 72340177 - sample 8: - time = 1137782 + sample 9: + time = 1144567 flags = 1 data = length 384, hash 9345E744 - sample 9: - time = 1161782 + sample 10: + time = 1168567 flags = 1 data = length 384, hash FDE39E3A - sample 10: - time = 1185782 + sample 11: + time = 1192567 flags = 1 data = length 384, hash F0B7465 - sample 11: - time = 1209782 + sample 12: + time = 1216567 flags = 1 data = length 384, hash 3693AB86 - sample 12: - time = 1233782 + sample 13: + time = 1240567 flags = 1 data = length 384, hash F39719B1 - sample 13: - time = 1257782 + sample 14: + time = 1264567 flags = 1 data = length 384, hash DA3958DC - sample 14: - time = 1281782 + sample 15: + time = 1288567 flags = 1 data = length 384, hash FDC7599F - sample 15: - time = 1305782 + sample 16: + time = 1312567 flags = 1 data = length 384, hash AEFF8471 - sample 16: - time = 1329782 + sample 17: + time = 1336567 flags = 1 data = length 384, hash 89C92C19 - sample 17: - time = 1353782 + sample 18: + time = 1360567 flags = 1 data = length 384, hash 5C786A4B - sample 18: - time = 1377782 + sample 19: + time = 1384567 flags = 1 data = length 384, hash 5ACA8B - sample 19: - time = 1401782 + sample 20: + time = 1408567 flags = 1 data = length 384, hash 7755974C - sample 20: - time = 1425782 + sample 21: + time = 1432567 flags = 1 data = length 384, hash 3934B73C - sample 21: - time = 1449782 + sample 22: + time = 1456567 flags = 1 data = length 384, hash DDD70A2F - sample 22: - time = 1473782 + sample 23: + time = 1480567 flags = 1 data = length 384, hash 8FACE2EF - sample 23: - time = 1497782 + sample 24: + time = 1504567 flags = 1 data = length 384, hash 4A602591 - sample 24: - time = 1521782 + sample 25: + time = 1528567 flags = 1 data = length 384, hash D019AA2D - sample 25: - time = 1545782 + sample 26: + time = 1552567 flags = 1 data = length 384, hash 8A680B9D - sample 26: - time = 1569782 + sample 27: + time = 1576567 flags = 1 data = length 384, hash B655C959 - sample 27: - time = 1593782 + sample 28: + time = 1600567 flags = 1 data = length 384, hash 2168336B - sample 28: - time = 1617782 + sample 29: + time = 1624567 flags = 1 data = length 384, hash D77F6D31 - sample 29: - time = 1641782 + sample 30: + time = 1648567 flags = 1 data = length 384, hash 524B4B2F - sample 30: - time = 1665782 + sample 31: + time = 1672567 flags = 1 data = length 384, hash 4752DDFC - sample 31: - time = 1689782 + sample 32: + time = 1696567 flags = 1 data = length 384, hash E786727F - sample 32: - time = 1713782 + sample 33: + time = 1720567 flags = 1 data = length 384, hash 5DA6FB8C - sample 33: - time = 1737782 + sample 34: + time = 1744567 flags = 1 data = length 384, hash 92F24269 - sample 34: - time = 1761782 + sample 35: + time = 1768567 flags = 1 data = length 384, hash CD0A3BA1 - sample 35: - time = 1785782 + sample 36: + time = 1792567 flags = 1 data = length 384, hash 7D00409F - sample 36: - time = 1809782 + sample 37: + time = 1816567 flags = 1 data = length 384, hash D7ADB5FA - sample 37: - time = 1833782 + sample 38: + time = 1840567 flags = 1 data = length 384, hash 4A140209 - sample 38: - time = 1857782 + sample 39: + time = 1864567 flags = 1 data = length 384, hash E801184A - sample 39: - time = 1881782 + sample 40: + time = 1888567 flags = 1 data = length 384, hash 53C6CF9C - sample 40: - time = 1905782 + sample 41: + time = 1912567 flags = 1 data = length 384, hash 19A8D99F - sample 41: - time = 1929782 + sample 42: + time = 1936567 flags = 1 data = length 384, hash E47EB43F - sample 42: - time = 1953782 + sample 43: + time = 1960567 flags = 1 data = length 384, hash 4EA329E7 - sample 43: - time = 1977782 + sample 44: + time = 1984567 flags = 1 data = length 384, hash 1CCAAE62 - sample 44: - time = 2001782 + sample 45: + time = 2008567 flags = 1 data = length 384, hash ED3F8C66 - sample 45: - time = 2025782 + sample 46: + time = 2032567 flags = 1 data = length 384, hash D3D646B6 - sample 46: - time = 2049782 + sample 47: + time = 2056567 flags = 1 data = length 384, hash 68CD1574 - sample 47: - time = 2073782 + sample 48: + time = 2080567 flags = 1 data = length 384, hash 8CEAB382 - sample 48: - time = 2097782 + sample 49: + time = 2104567 flags = 1 data = length 384, hash D54B1C48 - sample 49: - time = 2121782 + sample 50: + time = 2128567 flags = 1 data = length 384, hash FFE2EE90 - sample 50: - time = 2145782 + sample 51: + time = 2152567 flags = 1 data = length 384, hash BFE8A673 - sample 51: - time = 2169782 + sample 52: + time = 2176567 flags = 1 data = length 384, hash 978B1C92 - sample 52: - time = 2193782 + sample 53: + time = 2200567 flags = 1 data = length 384, hash 810CC71E - sample 53: - time = 2217782 + sample 54: + time = 2224567 flags = 1 data = length 384, hash 44FE42D9 - sample 54: - time = 2241782 + sample 55: + time = 2248567 flags = 1 data = length 384, hash 2F5BB02C - sample 55: - time = 2265782 + sample 56: + time = 2272567 flags = 1 data = length 384, hash 77DDB90 - sample 56: - time = 2289782 + sample 57: + time = 2296567 flags = 1 data = length 384, hash 24FB5EDA - sample 57: - time = 2313782 + sample 58: + time = 2320567 flags = 1 data = length 384, hash E73203C6 - sample 58: - time = 2337782 + sample 59: + time = 2344567 flags = 1 data = length 384, hash 14B525F1 - sample 59: - time = 2361782 + sample 60: + time = 2368567 flags = 1 data = length 384, hash 5E0F4E2E - sample 60: - time = 2385782 + sample 61: + time = 2392567 flags = 1 data = length 384, hash 67EE4E31 - sample 61: - time = 2409782 + sample 62: + time = 2416567 flags = 1 data = length 384, hash 2E04EC4C - sample 62: - time = 2433782 + sample 63: + time = 2440567 flags = 1 data = length 384, hash 852CABA7 - sample 63: - time = 2457782 + sample 64: + time = 2464567 flags = 1 data = length 384, hash 19928903 - sample 64: - time = 2481782 + sample 65: + time = 2488567 flags = 1 data = length 384, hash 5DA42021 - sample 65: - time = 2505782 + sample 66: + time = 2512567 flags = 1 data = length 384, hash 45B20B7C - sample 66: - time = 2529782 + sample 67: + time = 2536567 flags = 1 data = length 384, hash D108A215 - sample 67: - time = 2553782 + sample 68: + time = 2560567 flags = 1 data = length 384, hash BD25DB7C - sample 68: - time = 2577782 + sample 69: + time = 2584567 flags = 1 data = length 384, hash DA7F9861 - sample 69: - time = 2601782 + sample 70: + time = 2608567 flags = 1 data = length 384, hash CCD576F - sample 70: - time = 2625782 + sample 71: + time = 2632567 flags = 1 data = length 384, hash 405C1EB5 - sample 71: - time = 2649782 + sample 72: + time = 2656567 flags = 1 data = length 384, hash 6640B74E - sample 72: - time = 2673782 + sample 73: + time = 2680567 flags = 1 data = length 384, hash B4E5937A - sample 73: - time = 2697782 + sample 74: + time = 2704567 flags = 1 data = length 384, hash CEE17733 - sample 74: - time = 2721782 + sample 75: + time = 2728567 flags = 1 data = length 384, hash 2A0DA733 - sample 75: - time = 2745782 + sample 76: + time = 2752567 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index b3cb117cb2..3f393e768e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -27,155 +27,155 @@ track 0: initializationData: sample count = 38 sample 0: - time = 1858196 + time = 1871586 flags = 1 data = length 384, hash E801184A sample 1: - time = 1882196 + time = 1895586 flags = 1 data = length 384, hash 53C6CF9C sample 2: - time = 1906196 + time = 1919586 flags = 1 data = length 384, hash 19A8D99F sample 3: - time = 1930196 + time = 1943586 flags = 1 data = length 384, hash E47EB43F sample 4: - time = 1954196 + time = 1967586 flags = 1 data = length 384, hash 4EA329E7 sample 5: - time = 1978196 + time = 1991586 flags = 1 data = length 384, hash 1CCAAE62 sample 6: - time = 2002196 + time = 2015586 flags = 1 data = length 384, hash ED3F8C66 sample 7: - time = 2026196 + time = 2039586 flags = 1 data = length 384, hash D3D646B6 sample 8: - time = 2050196 + time = 2063586 flags = 1 data = length 384, hash 68CD1574 sample 9: - time = 2074196 + time = 2087586 flags = 1 data = length 384, hash 8CEAB382 sample 10: - time = 2098196 + time = 2111586 flags = 1 data = length 384, hash D54B1C48 sample 11: - time = 2122196 + time = 2135586 flags = 1 data = length 384, hash FFE2EE90 sample 12: - time = 2146196 + time = 2159586 flags = 1 data = length 384, hash BFE8A673 sample 13: - time = 2170196 + time = 2183586 flags = 1 data = length 384, hash 978B1C92 sample 14: - time = 2194196 + time = 2207586 flags = 1 data = length 384, hash 810CC71E sample 15: - time = 2218196 + time = 2231586 flags = 1 data = length 384, hash 44FE42D9 sample 16: - time = 2242196 + time = 2255586 flags = 1 data = length 384, hash 2F5BB02C sample 17: - time = 2266196 + time = 2279586 flags = 1 data = length 384, hash 77DDB90 sample 18: - time = 2290196 + time = 2303586 flags = 1 data = length 384, hash 24FB5EDA sample 19: - time = 2314196 + time = 2327586 flags = 1 data = length 384, hash E73203C6 sample 20: - time = 2338196 + time = 2351586 flags = 1 data = length 384, hash 14B525F1 sample 21: - time = 2362196 + time = 2375586 flags = 1 data = length 384, hash 5E0F4E2E sample 22: - time = 2386196 + time = 2399586 flags = 1 data = length 384, hash 67EE4E31 sample 23: - time = 2410196 + time = 2423586 flags = 1 data = length 384, hash 2E04EC4C sample 24: - time = 2434196 + time = 2447586 flags = 1 data = length 384, hash 852CABA7 sample 25: - time = 2458196 + time = 2471586 flags = 1 data = length 384, hash 19928903 sample 26: - time = 2482196 + time = 2495586 flags = 1 data = length 384, hash 5DA42021 sample 27: - time = 2506196 + time = 2519586 flags = 1 data = length 384, hash 45B20B7C sample 28: - time = 2530196 + time = 2543586 flags = 1 data = length 384, hash D108A215 sample 29: - time = 2554196 + time = 2567586 flags = 1 data = length 384, hash BD25DB7C sample 30: - time = 2578196 + time = 2591586 flags = 1 data = length 384, hash DA7F9861 sample 31: - time = 2602196 + time = 2615586 flags = 1 data = length 384, hash CCD576F sample 32: - time = 2626196 + time = 2639586 flags = 1 data = length 384, hash 405C1EB5 sample 33: - time = 2650196 + time = 2663586 flags = 1 data = length 384, hash 6640B74E sample 34: - time = 2674196 + time = 2687586 flags = 1 data = length 384, hash B4E5937A sample 35: - time = 2698196 + time = 2711586 flags = 1 data = length 384, hash CEE17733 sample 36: - time = 2722196 + time = 2735586 flags = 1 data = length 384, hash 2A0DA733 sample 37: - time = 2746196 + time = 2759586 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index e02e99e139..442e62deca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.Util; /** @@ -26,22 +27,21 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; - private final long dataSize; private final int frameSize; + private final long dataSize; private final int bitrate; private final long durationUs; /** - * @param firstFramePosition The position (byte offset) of the first frame. - * @param inputLength The length of the stream. - * @param frameSize The size of a single frame in the stream. - * @param bitrate The stream's bitrate. + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFramePosition The position of the first frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the first frame. */ - public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, - int bitrate) { + public ConstantBitrateSeeker(long inputLength, long firstFramePosition, + MpegAudioHeader mpegAudioHeader) { this.firstFramePosition = firstFramePosition; - this.frameSize = frameSize; - this.bitrate = bitrate; + this.frameSize = mpegAudioHeader.frameSize; + this.bitrate = mpegAudioHeader.bitrate; if (inputLength == C.LENGTH_UNSET) { dataSize = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 7c579504c3..5c56dc460a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor { int seekHeader = getSeekFrameHeader(frame, xingBase); Seeker seeker; if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { - seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor { return getConstantBitrateSeeker(input); } } else if (seekHeader == SEEK_HEADER_VBRI) { - seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); input.skipFully(synchronizedHeader.frameSize); } else { // seekerHeader == SEEK_HEADER_UNSET // This frame doesn't contain seeking information, so reset the peek position. @@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), - synchronizedHeader.frameSize, synchronizedHeader.bitrate); + return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index c43f065592..cc631d9f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class VbriSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "VbriSeeker"; + /** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { @@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util; int entrySize = frame.readUnsignedShort(); frame.skipBytes(2); - // Skip the frame containing the VBRI header. - position += mpegAudioHeader.frameSize; - + long minPosition = position + mpegAudioHeader.frameSize; // Read table of contents entries. - long[] timesUs = new long[entryCount + 1]; - long[] positions = new long[entryCount + 1]; - timesUs[0] = 0L; - positions[0] = position; - for (int index = 1; index < timesUs.length; index++) { + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); int segmentSize; switch (entrySize) { case 1: @@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util; return null; } position += segmentSize * scale; - timesUs[index] = index * durationUs / entryCount; - positions[index] = - inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); + } + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); } return new VbriSeeker(timesUs, positions, durationUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 55888066e7..e532249a64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class XingSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "XingSeeker"; + /** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'Xing' or 'Info' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; - long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); int frameCount; @@ -54,10 +56,10 @@ import com.google.android.exoplayer2.util.Util; sampleRate); if ((flags & 0x06) != 0x06) { // If the size in bytes or table of contents is missing, the stream is not seekable. - return new XingSeeker(firstFramePosition, durationUs, inputLength); + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long sizeBytes = frame.readUnsignedIntToInt(); + long dataSize = frame.readUnsignedIntToInt(); long[] tableOfContents = new long[100]; for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); @@ -66,32 +68,37 @@ import com.google.android.exoplayer2.util.Util; // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); - return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, - sizeBytes, mpegAudioHeader.frameSize); + + if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) { + Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize)); + } + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize, + tableOfContents); } - private final long firstFramePosition; + private final long dataStartPosition; + private final int xingFrameSize; private final long durationUs; - private final long inputLength; + /** + * Data size, including the XING frame. + */ + private final long dataSize; /** * Entries are in the range [0, 255], but are stored as long integers for convenience. */ private final long[] tableOfContents; - private final long sizeBytes; - private final int headerSize; - private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { - this(firstFramePosition, durationUs, inputLength, null, 0, 0); + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null); } - private XingSeeker(long firstFramePosition, long durationUs, long inputLength, - long[] tableOfContents, long sizeBytes, int headerSize) { - this.firstFramePosition = firstFramePosition; + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize, + long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.inputLength = inputLength; + this.dataSize = dataSize; this.tableOfContents = tableOfContents; - this.sizeBytes = sizeBytes; - this.headerSize = headerSize; } @Override @@ -102,44 +109,45 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (!isSeekable()) { - return firstFramePosition; + return dataStartPosition + xingFrameSize; } double percent = (timeUs * 100d) / durationUs; - double fx; + double scaledPosition; if (percent <= 0) { - fx = 0; + scaledPosition = 0; } else if (percent >= 100) { - fx = 256; + scaledPosition = 256; } else { - int a = (int) percent; - float fa = tableOfContents[a]; - float fb = a == 99 ? 256 : tableOfContents[a + 1]; - fx = fa + (fb - fa) * (percent - a); + int prevTableIndex = (int) percent; + double prevScaledPosition = tableOfContents[prevTableIndex]; + double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two scaled positions. + double interpolateFraction = percent - prevTableIndex; + scaledPosition = prevScaledPosition + + (interpolateFraction * (nextScaledPosition - prevScaledPosition)); } - - long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; - long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 - : firstFramePosition - headerSize + sizeBytes - 1; - return Math.min(position, maximumPosition); + long positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return dataStartPosition + positionOffset; } @Override public long getTimeUs(long position) { - if (!isSeekable() || position < firstFramePosition) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; - int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); - long previousTime = getTimeUsForTocPosition(previousTocPosition); - - // Linearly interpolate the time taking into account the next entry. - long previousByte = tableOfContents[previousTocPosition]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; - long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); - long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) - * (offsetByte - previousByte) / (nextByte - previousByte)); - return previousTime + timeOffset; + double scaledPosition = (positionOffset * 256d) / dataSize; + int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); + long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); + long prevScaledPosition = tableOfContents[prevTableIndex]; + long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1); + long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two table entries. + double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0 + : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition)); + return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs)); } @Override @@ -148,11 +156,13 @@ import com.google.android.exoplayer2.util.Util; } /** - * Returns the time in microseconds corresponding to a table of contents position, which is - * interpreted as a percentage of the stream's duration between 0 and 100. + * Returns the time in microseconds for a given table index. + * + * @param tableIndex A table index in the range [0, 100]. + * @return The corresponding time in microseconds. */ - private long getTimeUsForTocPosition(int tocPosition) { - return (durationUs * tocPosition) / 100; + private long getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index b43949b7c2..e644abc7ef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -43,17 +43,17 @@ public final class XingSeekerTest { private static final int XING_FRAME_POSITION = 157; /** - * Size of the audio stream, encoded in {@link #XING_FRAME_PAYLOAD}. + * Data size, as encoded in {@link #XING_FRAME_PAYLOAD}. */ - private static final int STREAM_SIZE_BYTES = 948505; + private static final int DATA_SIZE_BYTES = 948505; /** * Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}. */ private static final int STREAM_DURATION_US = 59271836; /** - * The length of the file in bytes. + * The length of the stream in bytes. */ - private static final int INPUT_LENGTH = 948662; + private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES; private XingSeeker seeker; private XingSeeker seekerWithInputLength; @@ -63,10 +63,10 @@ public final class XingSeekerTest { public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); - seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), - XING_FRAME_POSITION, C.LENGTH_UNSET); - seekerWithInputLength = XingSeeker.create(xingFrameHeader, - new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); + seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader, + new ParsableByteArray(XING_FRAME_PAYLOAD)); + seekerWithInputLength = XingSeeker.create(STREAM_LENGTH, + XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)); xingFrameSize = xingFrameHeader.frameSize; } @@ -84,10 +84,10 @@ public final class XingSeekerTest { @Test public void testGetTimeUsAtEndOfStream() { - assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + assertThat(seeker.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); assertThat( - seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + seekerWithInputLength.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); } @@ -100,14 +100,14 @@ public final class XingSeekerTest { @Test public void testGetPositionAtEndOfStream() { assertThat(seeker.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); } @Test public void testGetTimeForAllPositions() { - for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { + for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; long timeUs = seeker.getTimeUs(position); assertThat(seeker.getPosition(timeUs)).isEqualTo(position); From b61d09c4166c798bd242cb3fa573de77b579ff88 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 08:36:03 -0800 Subject: [PATCH 102/148] Fix mp3 extractor test ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177458840 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 7b6fe9db37..a57894e81e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -27,311 +27,311 @@ track 0: initializationData: sample count = 77 sample 0: - time = 928567 + time = 928568 flags = 1 data = length 384, hash F7E344F4 sample 1: - time = 952567 + time = 952568 flags = 1 data = length 384, hash 14EF6AFD sample 2: - time = 976567 + time = 976568 flags = 1 data = length 384, hash 61C9B92C sample 3: - time = 1000567 + time = 1000568 flags = 1 data = length 384, hash ABE1368 sample 4: - time = 1024567 + time = 1024568 flags = 1 data = length 384, hash 6A3B8547 sample 5: - time = 1048567 + time = 1048568 flags = 1 data = length 384, hash 30E905FA sample 6: - time = 1072567 + time = 1072568 flags = 1 data = length 384, hash 21A267CD sample 7: - time = 1096567 + time = 1096568 flags = 1 data = length 384, hash D96A2651 sample 8: - time = 1120567 + time = 1120568 flags = 1 data = length 384, hash 72340177 sample 9: - time = 1144567 + time = 1144568 flags = 1 data = length 384, hash 9345E744 sample 10: - time = 1168567 + time = 1168568 flags = 1 data = length 384, hash FDE39E3A sample 11: - time = 1192567 + time = 1192568 flags = 1 data = length 384, hash F0B7465 sample 12: - time = 1216567 + time = 1216568 flags = 1 data = length 384, hash 3693AB86 sample 13: - time = 1240567 + time = 1240568 flags = 1 data = length 384, hash F39719B1 sample 14: - time = 1264567 + time = 1264568 flags = 1 data = length 384, hash DA3958DC sample 15: - time = 1288567 + time = 1288568 flags = 1 data = length 384, hash FDC7599F sample 16: - time = 1312567 + time = 1312568 flags = 1 data = length 384, hash AEFF8471 sample 17: - time = 1336567 + time = 1336568 flags = 1 data = length 384, hash 89C92C19 sample 18: - time = 1360567 + time = 1360568 flags = 1 data = length 384, hash 5C786A4B sample 19: - time = 1384567 + time = 1384568 flags = 1 data = length 384, hash 5ACA8B sample 20: - time = 1408567 + time = 1408568 flags = 1 data = length 384, hash 7755974C sample 21: - time = 1432567 + time = 1432568 flags = 1 data = length 384, hash 3934B73C sample 22: - time = 1456567 + time = 1456568 flags = 1 data = length 384, hash DDD70A2F sample 23: - time = 1480567 + time = 1480568 flags = 1 data = length 384, hash 8FACE2EF sample 24: - time = 1504567 + time = 1504568 flags = 1 data = length 384, hash 4A602591 sample 25: - time = 1528567 + time = 1528568 flags = 1 data = length 384, hash D019AA2D sample 26: - time = 1552567 + time = 1552568 flags = 1 data = length 384, hash 8A680B9D sample 27: - time = 1576567 + time = 1576568 flags = 1 data = length 384, hash B655C959 sample 28: - time = 1600567 + time = 1600568 flags = 1 data = length 384, hash 2168336B sample 29: - time = 1624567 + time = 1624568 flags = 1 data = length 384, hash D77F6D31 sample 30: - time = 1648567 + time = 1648568 flags = 1 data = length 384, hash 524B4B2F sample 31: - time = 1672567 + time = 1672568 flags = 1 data = length 384, hash 4752DDFC sample 32: - time = 1696567 + time = 1696568 flags = 1 data = length 384, hash E786727F sample 33: - time = 1720567 + time = 1720568 flags = 1 data = length 384, hash 5DA6FB8C sample 34: - time = 1744567 + time = 1744568 flags = 1 data = length 384, hash 92F24269 sample 35: - time = 1768567 + time = 1768568 flags = 1 data = length 384, hash CD0A3BA1 sample 36: - time = 1792567 + time = 1792568 flags = 1 data = length 384, hash 7D00409F sample 37: - time = 1816567 + time = 1816568 flags = 1 data = length 384, hash D7ADB5FA sample 38: - time = 1840567 + time = 1840568 flags = 1 data = length 384, hash 4A140209 sample 39: - time = 1864567 + time = 1864568 flags = 1 data = length 384, hash E801184A sample 40: - time = 1888567 + time = 1888568 flags = 1 data = length 384, hash 53C6CF9C sample 41: - time = 1912567 + time = 1912568 flags = 1 data = length 384, hash 19A8D99F sample 42: - time = 1936567 + time = 1936568 flags = 1 data = length 384, hash E47EB43F sample 43: - time = 1960567 + time = 1960568 flags = 1 data = length 384, hash 4EA329E7 sample 44: - time = 1984567 + time = 1984568 flags = 1 data = length 384, hash 1CCAAE62 sample 45: - time = 2008567 + time = 2008568 flags = 1 data = length 384, hash ED3F8C66 sample 46: - time = 2032567 + time = 2032568 flags = 1 data = length 384, hash D3D646B6 sample 47: - time = 2056567 + time = 2056568 flags = 1 data = length 384, hash 68CD1574 sample 48: - time = 2080567 + time = 2080568 flags = 1 data = length 384, hash 8CEAB382 sample 49: - time = 2104567 + time = 2104568 flags = 1 data = length 384, hash D54B1C48 sample 50: - time = 2128567 + time = 2128568 flags = 1 data = length 384, hash FFE2EE90 sample 51: - time = 2152567 + time = 2152568 flags = 1 data = length 384, hash BFE8A673 sample 52: - time = 2176567 + time = 2176568 flags = 1 data = length 384, hash 978B1C92 sample 53: - time = 2200567 + time = 2200568 flags = 1 data = length 384, hash 810CC71E sample 54: - time = 2224567 + time = 2224568 flags = 1 data = length 384, hash 44FE42D9 sample 55: - time = 2248567 + time = 2248568 flags = 1 data = length 384, hash 2F5BB02C sample 56: - time = 2272567 + time = 2272568 flags = 1 data = length 384, hash 77DDB90 sample 57: - time = 2296567 + time = 2296568 flags = 1 data = length 384, hash 24FB5EDA sample 58: - time = 2320567 + time = 2320568 flags = 1 data = length 384, hash E73203C6 sample 59: - time = 2344567 + time = 2344568 flags = 1 data = length 384, hash 14B525F1 sample 60: - time = 2368567 + time = 2368568 flags = 1 data = length 384, hash 5E0F4E2E sample 61: - time = 2392567 + time = 2392568 flags = 1 data = length 384, hash 67EE4E31 sample 62: - time = 2416567 + time = 2416568 flags = 1 data = length 384, hash 2E04EC4C sample 63: - time = 2440567 + time = 2440568 flags = 1 data = length 384, hash 852CABA7 sample 64: - time = 2464567 + time = 2464568 flags = 1 data = length 384, hash 19928903 sample 65: - time = 2488567 + time = 2488568 flags = 1 data = length 384, hash 5DA42021 sample 66: - time = 2512567 + time = 2512568 flags = 1 data = length 384, hash 45B20B7C sample 67: - time = 2536567 + time = 2536568 flags = 1 data = length 384, hash D108A215 sample 68: - time = 2560567 + time = 2560568 flags = 1 data = length 384, hash BD25DB7C sample 69: - time = 2584567 + time = 2584568 flags = 1 data = length 384, hash DA7F9861 sample 70: - time = 2608567 + time = 2608568 flags = 1 data = length 384, hash CCD576F sample 71: - time = 2632567 + time = 2632568 flags = 1 data = length 384, hash 405C1EB5 sample 72: - time = 2656567 + time = 2656568 flags = 1 data = length 384, hash 6640B74E sample 73: - time = 2680567 + time = 2680568 flags = 1 data = length 384, hash B4E5937A sample 74: - time = 2704567 + time = 2704568 flags = 1 data = length 384, hash CEE17733 sample 75: - time = 2728567 + time = 2728568 flags = 1 data = length 384, hash 2A0DA733 sample 76: - time = 2752567 + time = 2752568 flags = 1 data = length 384, hash 97F4129B tracksEnded = true From 393a7625630e50dcbb84b7c653d4d61f1168725e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 01:18:58 -0800 Subject: [PATCH 103/148] Use AdaptiveMediaSourceEventListener for ExtractorMediaSource This is a step towards harmonizing the MediaSource Builders and (potentially) providing MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177783157 --- RELEASENOTES.md | 2 + .../android/exoplayer2/demo/EventLogger.java | 24 +- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 14 +- .../AdaptiveMediaSourceEventListener.java | 305 +---------- .../source/ExtractorMediaPeriod.java | 62 ++- .../source/ExtractorMediaSource.java | 159 +++++- .../source/MediaSourceEventListener.java | 487 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 81 ++- .../android/exoplayer2/upstream/DataSpec.java | 19 +- 9 files changed, 753 insertions(+), 400 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a123c78323..3a73a9e716 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * Fix handling of playback parameters changes while paused when followed by a seek. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 4819c28753..b9be8f3846 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -53,13 +52,15 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; -/** - * Logs player events using {@link Log}. - */ -/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, - DefaultDrmSessionManager.EventListener { +/** Logs player events using {@link Log}. */ +/* package */ final class EventLogger + implements Player.EventListener, + MetadataOutput, + AudioRendererEventListener, + VideoRendererEventListener, + AdaptiveMediaSourceEventListener, + AdsMediaSource.EventListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -322,13 +323,6 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // ExtractorMediaSource.EventListener - - @Override - public void onLoadError(IOException error) { - printInternalError("loadError", error); - } - // AdaptiveMediaSourceEventListener @Override diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 02aa4807a5..cd646daf42 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -62,9 +62,13 @@ public final class ImaAdsMediaSource implements MediaSource { * @param eventHandler A handler for events. 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. */ - public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsMediaSource.AdsListener eventListener) { + public ImaAdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + ImaAdsLoader imaAdsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable AdsMediaSource.EventListener eventListener) { adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, eventHandler, eventListener); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index be07cbb5dc..2bc9d48726 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -16,306 +16,39 @@ package com.google.android.exoplayer2.source; import android.os.Handler; -import android.os.SystemClock; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; +import android.support.annotation.Nullable; /** - * Interface for callbacks to be notified of adaptive {@link MediaSource} events. + * Interface for callbacks to be notified of {@link MediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener} */ -public interface AdaptiveMediaSourceEventListener { +@Deprecated +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - /** - * Called when a load begins. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. - */ - void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs); - - /** - * Called when a load ends. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. - * @param bytesLoaded The number of bytes that were loaded. - */ - void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load is canceled. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was - * canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. - * @param bytesLoaded The number of bytes that were loaded prior to cancelation. - */ - void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load error occurs. - *

    - * The error may or may not have resulted in the load being canceled, as indicated by the - * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will - * not be called in addition to this method. - *

    - * This method being called does not indicate that playback has failed, or that it will fail. The - * player may be able to recover from the error and continue. Hence applications should - * not implement this method to display a user visible error or initiate an application - * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement - * such behavior). This method is called to provide the application with an opportunity to log the - * error if it wishes to do so. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error - * occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. - * @param bytesLoaded The number of bytes that were loaded prior to the error. - * @param error The load error. - * @param wasCanceled Whether the load was canceled as a result of the error. - */ - void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled); - - /** - * Called when data is removed from the back of a media buffer, typically so that it can be - * re-buffered in a different format. - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param mediaStartTimeMs The start time of the media being discarded. - * @param mediaEndTimeMs The end time of the media being discarded. - */ - void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); - - /** - * Called when a downstream format change occurs (i.e. when the format of the media being read - * from one or more {@link SampleStream}s provided by the source changes). - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaTimeMs The media time at which the change occurred. - */ - void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs); - - /** - * Dispatches events to a {@link AdaptiveMediaSourceEventListener}. - */ - final class EventDispatcher { + /** Dispatches events to a {@link MediaSourceEventListener}. */ + final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { private final Handler handler; - private final AdaptiveMediaSourceEventListener listener; - private final long mediaTimeOffsetMs; + private final MediaSourceEventListener listener; - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) { + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { this(handler, listener, 0); } - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener, + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, long mediaTimeOffsetMs) { - this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + super(handler, listener, mediaTimeOffsetMs); + this.handler = handler; this.listener = listener; - this.mediaTimeOffsetMs = mediaTimeOffsetMs; } - public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { - return new EventDispatcher(handler, listener, mediaTimeOffsetMs); - } - - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { - loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs); - } - - public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs); - } - }); - } - } - - public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { - loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - - public void loadError(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded, final IOException error, - final boolean wasCanceled) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - }); - } - } - - public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs, - final long mediaEndTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs)); - } - }); - } - } - - public void downstreamFormatChanged(final int trackType, final Format trackFormat, - final int trackSelectionReason, final Object trackSelectionData, - final long mediaTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaTimeUs)); - } - }); - } - } - - private long adjustMediaTime(long mediaTimeUs) { - long mediaTimeMs = C.usToMs(mediaTimeUs); - return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( + long mediaTimeOffsetMs) { + return new AdaptiveMediaSourceEventListener.EventDispatcher( + handler, listener, mediaTimeOffsetMs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index d43b2d87b2..e3c9012dbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -74,11 +76,10 @@ import java.util.Arrays; private final Uri uri; private final DataSource dataSource; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final ExtractorMediaSource.EventListener eventListener; + private final EventDispatcher eventDispatcher; private final Listener listener; private final Allocator allocator; - private final String customCacheKey; + @Nullable private final String customCacheKey; private final long continueLoadingCheckIntervalBytes; private final Loader loader; private final ExtractorHolder extractorHolder; @@ -117,8 +118,7 @@ import java.util.Arrays; * @param dataSource The data source to read the media. * @param extractors The extractors to use to read the data source. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @param eventHandler A handler for events. 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 eventDispatcher A dispatcher to notify of events. * @param listener A listener to notify when information about the period changes. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache @@ -126,15 +126,20 @@ import java.util.Arrays; * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. */ - public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, - int minLoadableRetryCount, Handler eventHandler, - ExtractorMediaSource.EventListener eventListener, Listener listener, - Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaPeriod( + Uri uri, + DataSource dataSource, + Extractor[] extractors, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + Listener listener, + Allocator allocator, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = eventDispatcher; this.listener = listener; this.allocator = allocator; this.customCacheKey = customCacheKey; @@ -430,8 +435,22 @@ import java.util.Arrays; public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { copyLengthFromLoader(loadable); - notifyLoadError(error); - if (isLoadableExceptionFatal(error)) { + boolean isErrorFatal = isLoadableExceptionFatal(error); + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded, + error, + /* wasCanceled= */ isErrorFatal); + if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } int extractedSamplesCount = getExtractedSamplesCount(); @@ -607,17 +626,6 @@ import java.util.Arrays; return e instanceof UnrecognizedInputFormatException; } - private void notifyLoadError(final IOException error) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(error); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private final int track; @@ -664,7 +672,9 @@ import java.util.Arrays; private boolean pendingExtractorSeek; private long seekTimeUs; + private DataSpec dataSpec; private long length; + private long bytesLoaded; public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, ConditionVariable loadCondition) { @@ -700,7 +710,8 @@ import java.util.Arrays; ExtractorInput input = null; try { long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey)); + dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey); + length = dataSource.open(dataSpec); if (length != C.LENGTH_UNSET) { length += position; } @@ -724,6 +735,7 @@ import java.util.Arrays; result = Extractor.RESULT_CONTINUE; } else if (input != null) { positionHolder.position = input.getPosition(); + bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition; } Util.closeQuietly(dataSource); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 066953b998..3ab2609c0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -17,14 +17,18 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -40,10 +44,12 @@ import java.io.IOException; * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { - /** * Listener of {@link ExtractorMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -89,8 +95,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; + private final EventDispatcher eventDispatcher; private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -108,9 +113,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private ExtractorsFactory extractorsFactory; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private String customCacheKey; + @Nullable private Handler eventHandler; + @Nullable private MediaSourceEventListener eventListener; + @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; private boolean isBuildCalled; @@ -187,8 +192,24 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventHandler A handler for events. * @param eventListener A listener of events. * @return This builder. + * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. */ + @Deprecated public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -270,12 +291,31 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + this( + uri, + dataSourceFactory, + extractorsFactory, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener), + customCacheKey, + continueLoadingCheckIntervalBytes); + } + + private ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; } @@ -294,9 +334,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), - extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); + return new ExtractorMediaPeriod( + uri, + dataSourceFactory.createDataSource(), + extractorsFactory.createExtractors(), + minLoadableRetryCount, + eventDispatcher, + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); } @Override @@ -331,4 +378,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + private final EventListener eventListener; + + public EventListenerWrapper(EventListener eventListener) { + this.eventListener = Assertions.checkNotNull(eventListener); + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java new file mode 100644 index 0000000000..82e8781d70 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** Interface for callbacks to be notified of {@link MediaSource} events. */ +public interface MediaSourceEventListener { + /** + * Called when a load begins. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. + */ + void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs); + + /** + * Called when a load ends. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. + * @param loadDurationMs The duration of the load. + * @param bytesLoaded The number of bytes that were loaded. + */ + void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load is canceled. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was + * canceled. + * @param loadDurationMs The duration of the load up to the point at which it was canceled. + * @param bytesLoaded The number of bytes that were loaded prior to cancelation. + */ + void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load error occurs. + * + *

    The error may or may not have resulted in the load being canceled, as indicated by the + * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will + * not be called in addition to this method. + * + *

    This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error + * occurred. + * @param loadDurationMs The duration of the load up to the point at which the error occurred. + * @param bytesLoaded The number of bytes that were loaded prior to the error. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled); + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param mediaStartTimeMs The start time of the media being discarded. + * @param mediaEndTimeMs The end time of the media being discarded. + */ + void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); + + /** + * Called when a downstream format change occurs (i.e. when the format of the media being read + * from one or more {@link SampleStream}s provided by the source changes). + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaTimeMs The media time at which the change occurred. + */ + void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs); + + /** Dispatches events to a {@link MediaSourceEventListener}. */ + class EventDispatcher { + + @Nullable private final Handler handler; + @Nullable private final MediaSourceEventListener listener; + private final long mediaTimeOffsetMs; + + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + this(handler, listener, 0); + } + + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, + long mediaTimeOffsetMs) { + this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + this.listener = listener; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + loadStarted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs); + } + + public void loadStarted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadStarted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs); + } + }); + } + } + + public void loadCompleted( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCompleted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCompleted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadCanceled( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCanceled( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCanceled( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadError( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + + public void loadError( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded, + final IOException error, + final boolean wasCanceled) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadError( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + }); + } + } + + public void upstreamDiscarded( + final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onUpstreamDiscarded( + trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs)); + } + }); + } + } + + public void downstreamFormatChanged( + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onDownstreamFormatChanged( + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaTimeUs)); + } + }); + } + } + + private long adjustMediaTime(long mediaTimeUs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 47a2540c38..54a8fd96ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -26,9 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; @@ -44,10 +44,8 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { - /** - * Listener for events relating to ad loading. - */ - public interface AdsListener { + /** Listener for ads media source events. */ + public interface EventListener extends MediaSourceEventListener { /** * Called if there was an error loading ads. The media source will load the content without ads @@ -75,15 +73,13 @@ public final class AdsMediaSource implements MediaSource { private final MediaSource contentMediaSource; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; + @Nullable private final Handler eventHandler; + @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; private final AdMediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; - @Nullable - private final Handler eventHandler; - @Nullable - private final AdsListener eventListener; private Handler playerHandler; private ExoPlayer player; @@ -115,10 +111,10 @@ public final class AdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

    - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + *

    Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. @@ -128,9 +124,13 @@ public final class AdsMediaSource implements MediaSource { * @param eventHandler A handler for events. 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. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsListener eventListener) { + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; @@ -186,7 +186,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); + adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -306,11 +306,8 @@ public final class AdsMediaSource implements MediaSource { } } - /** - * Listener for component events. All methods are called on the main thread. - */ - private final class ComponentListener implements AdsLoader.EventListener, - AdMediaSourceLoadErrorListener { + /** Listener for component events. All methods are called on the main thread. */ + private final class ComponentListener implements AdsLoader.EventListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -374,20 +371,6 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Listener for errors while loading an ad {@link MediaSource}. - */ - private interface AdMediaSourceLoadErrorListener { - - /** - * Called when an error occurs loading media data. - * - * @param error The load error. - */ - void onLoadError(IOException error); - - } - /** * Factory for {@link MediaSource}s for loading ad media. */ @@ -397,15 +380,13 @@ public final class AdsMediaSource implements MediaSource { * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. * * @param uri The URI of the ad. - * @param handler A handler for listener events. - * @param listener A listener for ad load errors. To have ad media source load errors notified - * via the ads media source's listener, call this listener's onLoadError method from your - * new media source's load error listener using the specified {@code handler}. Otherwise, - * this parameter can be ignored. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. * @return The new media source. */ - MediaSource createAdMediaSource(Uri uri, Handler handler, - AdMediaSourceLoadErrorListener listener); + MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); /** * Returns the content types supported by media sources created by this factory. Each element @@ -427,15 +408,11 @@ public final class AdsMediaSource implements MediaSource { } @Override - public MediaSource createAdMediaSource(Uri uri, Handler handler, - final AdMediaSourceLoadErrorListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, - new EventListener() { - @Override - public void onLoadError(IOException error) { - listener.onLoadError(error); - } - }).build(); + public MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory) + .setEventListener(handler, listener) + .build(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index ab1542c7a6..cbe971bc5d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Retention; @@ -79,7 +80,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * {@link DataSpec} is not intended to be used in conjunction with a cache. */ - public final String key; + @Nullable public final String key; /** * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. @@ -113,7 +114,7 @@ public final class DataSpec { * @param length {@link #length}. * @param key {@link #key}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); } @@ -147,8 +148,8 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} may differ from - * {@link #absoluteStreamPosition}. + * Construct a {@link DataSpec} where {@link #position} may differ from {@link + * #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param postBody {@link #postBody}. @@ -158,8 +159,14 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, - String key, @Flags int flags) { + public DataSpec( + Uri uri, + byte[] postBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); From e759462af8b2d6df4ff2f378cf216e409ae8e7b2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 02:06:42 -0800 Subject: [PATCH 104/148] Update internal usages of deprecated AdaptiveMediaSourceEventListener ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177786580 --- .../android/exoplayer2/demo/EventLogger.java | 21 +++++++---- .../AdaptiveMediaSourceEventListener.java | 36 ++----------------- .../source/ExtractorMediaSource.java | 2 +- .../source/MediaSourceEventListener.java | 6 +++- .../source/chunk/ChunkSampleStream.java | 3 +- .../source/dash/DashMediaPeriod.java | 2 +- .../source/dash/DashMediaSource.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 2 +- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 2 +- .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../testutil/FakeAdaptiveMediaSource.java | 14 +++++--- 15 files changed, 41 insertions(+), 59 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index b9be8f3846..6fe0f15232 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -58,7 +58,7 @@ import java.util.Locale; MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, - AdaptiveMediaSourceEventListener, + MediaSourceEventListener, AdsMediaSource.EventListener, DefaultDrmSessionManager.EventListener { @@ -323,12 +323,19 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // AdaptiveMediaSourceEventListener + // MediaSourceEventListener @Override - public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs) { + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { // Do nothing. } @@ -369,7 +376,7 @@ import java.util.Locale; @Override public void onAdLoadError(IOException error) { - printInternalError("loadError", error); + printInternalError("adLoadError", error); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index 2bc9d48726..ccc3beac55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -15,42 +15,10 @@ */ package com.google.android.exoplayer2.source; -import android.os.Handler; -import android.support.annotation.Nullable; - /** * Interface for callbacks to be notified of {@link MediaSource} events. * - * @deprecated Use {@link MediaSourceEventListener} + * @deprecated Use {@link MediaSourceEventListener}. */ @Deprecated -public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - - /** Dispatches events to a {@link MediaSourceEventListener}. */ - final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { - - private final Handler handler; - private final MediaSourceEventListener listener; - - public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - this(handler, listener, 0); - } - - public EventDispatcher( - @Nullable Handler handler, - @Nullable MediaSourceEventListener listener, - long mediaTimeOffsetMs) { - super(handler, listener, mediaTimeOffsetMs); - this.handler = handler; - this.listener = listener; - } - - public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( - long mediaTimeOffsetMs) { - return new AdaptiveMediaSourceEventListener.EventDispatcher( - handler, listener, mediaTimeOffsetMs); - } - - } - -} +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 3ab2609c0e..247eacd519 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 82e8781d70..4d500f94bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -211,7 +211,7 @@ public interface MediaSourceEventListener { long mediaTimeMs); /** Dispatches events to a {@link MediaSourceEventListener}. */ - class EventDispatcher { + final class EventDispatcher { @Nullable private final Handler handler; @Nullable private final MediaSourceEventListener listener; @@ -230,6 +230,10 @@ public interface MediaSourceEventListener { this.mediaTimeOffsetMs = mediaTimeOffsetMs; } + public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { + return new EventDispatcher(handler, listener, mediaTimeOffsetMs); + } + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, 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 bb51ae074e..fa95269690 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 @@ -16,12 +16,11 @@ package com.google.android.exoplayer2.source.chunk; import android.util.Log; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 35f3c2e129..3c401624db 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -19,10 +19,10 @@ import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index c5fbafb84e..498c7c6de3 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -27,9 +27,9 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index ea9e52e62e..bd73ad27f9 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -19,9 +19,9 @@ import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 3f28981f0e..563f4da059 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -22,9 +22,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index ddd6689fa6..beaa84556b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleStream; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 355a8575ca..0677ff7ca0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -20,7 +20,7 @@ import android.os.Handler; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 1cc2a6833d..9b664d8a61 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 5a93847428..5a26585874 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -25,9 +25,9 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 3dcf551943..1b2e1af9b7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.testutil; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroupArray; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 59bcaf3e7c..fbb2a83027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; @@ -33,9 +33,13 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { private final EventDispatcher eventDispatcher; private final FakeChunkSource.Factory chunkSourceFactory; - public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + public FakeAdaptiveMediaSource( + Timeline timeline, + Object manifest, + TrackGroupArray trackGroupArray, + Handler eventHandler, + MediaSourceEventListener eventListener, + FakeChunkSource.Factory chunkSourceFactory) { super(timeline, manifest, trackGroupArray); this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.chunkSourceFactory = chunkSourceFactory; From 10b24be6f089b77713e5f1b1fc1c431aa2b83599 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Dec 2017 21:40:58 +0000 Subject: [PATCH 105/148] Fix build --- .../source/DeferredMediaPeriod.java | 4 +-- .../source/dash/DashMediaSource.java | 23 +++++++--------- .../exoplayer2/source/hls/HlsMediaSource.java | 26 +++++++++---------- .../source/smoothstreaming/SsMediaSource.java | 26 ++++++++----------- 4 files changed, 34 insertions(+), 45 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index bc29b2fdf1..f93d30cb04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -95,8 +95,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 498c7c6de3..548811cf92 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -26,9 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; @@ -69,7 +69,7 @@ public final class DashMediaSource implements MediaSource { private final DashChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; @@ -151,8 +151,7 @@ public final class DashMediaSource implements MediaSource { * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -261,7 +260,7 @@ public final class DashMediaSource implements MediaSource { */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -278,8 +277,7 @@ public final class DashMediaSource implements MediaSource { */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener - eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); } @@ -299,7 +297,7 @@ public final class DashMediaSource implements MediaSource { @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -325,8 +323,7 @@ public final class DashMediaSource implements MediaSource { @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -353,8 +350,7 @@ public final class DashMediaSource implements MediaSource { public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -363,8 +359,7 @@ public final class DashMediaSource implements MediaSource { DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this.manifest = manifest; this.manifestUri = manifestUri; this.manifestDataSourceFactory = manifestDataSourceFactory; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 563f4da059..4e904032fd 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -21,9 +21,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -57,7 +57,7 @@ public final class HlsMediaSource implements MediaSource, private HlsExtractorFactory extractorFactory; private ParsingLoadable.Parser playlistParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; private boolean isBuildCalled; @@ -132,7 +132,7 @@ public final class HlsMediaSource implements MediaSource, * @return This builder. */ public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -193,13 +193,13 @@ public final class HlsMediaSource implements MediaSource, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -211,14 +211,13 @@ public final class HlsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times loads must be retried before * errors are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); @@ -232,16 +231,15 @@ public final class HlsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times loads must be retried before * errors are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @deprecated Use {@link Builder} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 5a26585874..34343e08e3 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -24,9 +24,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; @@ -63,7 +63,7 @@ public final class SsMediaSource implements MediaSource, private final SsChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; @@ -143,8 +143,7 @@ public final class SsMediaSource implements MediaSource, * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -233,9 +232,9 @@ public final class SsMediaSource implements MediaSource, */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + Handler eventHandler, MediaSourceEventListener eventListener) { + this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + eventListener); } /** @@ -250,8 +249,7 @@ public final class SsMediaSource implements MediaSource, */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); } @@ -271,7 +269,7 @@ public final class SsMediaSource implements MediaSource, @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -295,8 +293,7 @@ public final class SsMediaSource implements MediaSource, @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -321,8 +318,7 @@ public final class SsMediaSource implements MediaSource, public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -332,7 +328,7 @@ public final class SsMediaSource implements MediaSource, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null From a8298b4c563b7df7d80125882dd358a73d7a688d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:08:33 -0800 Subject: [PATCH 106/148] Tentative fix for roll-up row count Issue: #3513 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177804505 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3a73a9e716..51ac077cef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,8 @@ seek. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index e2c592be6b..0483f909b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; /** @@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder { private final ParsableByteArray ccData; private final int packetLength; private final int selectedField; - private final LinkedList cueBuilders; + private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; private List cues; @@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder { public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - cueBuilders = new LinkedList<>(); + cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { @@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder { cues = null; lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder { private void handleMiscCode(byte cc2) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: - captionRowCount = 2; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(2); return; case CTRL_ROLL_UP_CAPTIONS_3_ROWS: - captionRowCount = 3; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(3); return; case CTRL_ROLL_UP_CAPTIONS_4_ROWS: - captionRowCount = 4; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(4); return; case CTRL_RESUME_CAPTION_LOADING: setCaptionMode(CC_MODE_POP_ON); @@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_RESUME_DIRECT_CAPTIONING: setCaptionMode(CC_MODE_PAINT_ON); return; + default: + // Fall through. + break; } if (captionMode == CC_MODE_UNKNOWN) { @@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_DELETE_TO_END_OF_ROW: // TODO: implement break; + default: + // Fall through. + break; } } @@ -515,8 +520,13 @@ public final class Cea608Decoder extends CeaDecoder { } } + private void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + currentCueBuilder.setCaptionRowCount(captionRowCount); + } + private void resetCueBuilders() { - currentCueBuilder.reset(captionMode, captionRowCount); + currentCueBuilder.reset(captionMode); cueBuilders.clear(); cueBuilders.add(currentCueBuilder); } @@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder { public CueBuilder(int captionMode, int captionRowCount) { preambleStyles = new ArrayList<>(); midrowStyles = new ArrayList<>(); - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); - reset(captionMode, captionRowCount); + reset(captionMode); + setCaptionRowCount(captionRowCount); } - public void reset(int captionMode, int captionRowCount) { + public void reset(int captionMode) { + this.captionMode = captionMode; preambleStyles.clear(); midrowStyles.clear(); rolledUpCaptions.clear(); @@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder { row = BASE_ROW; indent = 0; tabOffset = 0; - this.captionMode = captionMode; - this.captionRowCount = captionRowCount; underlineStartPosition = POSITION_UNSET; } + public void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + } + public boolean isEmpty() { return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; From 29f6351b192b192618bff721f63fc8c14669ea9a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:56:04 -0800 Subject: [PATCH 107/148] Support timezone offsets in ISO8601 timestamps Issue: #3524 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177808106 --- RELEASENOTES.md | 3 + .../source/dash/DashMediaSource.java | 56 ++++++++++--------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 51ac077cef..a55044ad7e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, DashMediaSource, SingleSampleMediaSource. +* DASH: + * Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 548811cf92..22bc2b08ec 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -48,6 +49,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A DASH {@link MediaSource}. @@ -926,41 +929,42 @@ public final class DashMediaSource implements MediaSource { } - private static final class Iso8601Parser implements ParsingLoadable.Parser { + /* package */ static final class Iso8601Parser implements ParsingLoadable.Parser { - private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; + private static final Pattern TIMESTAMP_WITH_TIMEZONE_PATTERN = + Pattern.compile("(.+?)(Z|((\\+|-|−)(\\d\\d)(:?(\\d\\d))?))"); @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - - if (firstLine != null) { - //determine format pattern - String formatPattern; - if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; - } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + try { + Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); + if (!matcher.matches()) { + throw new ParserException("Couldn't parse timestamp: " + firstLine); + } + // Parse the timestamp. + String timestampWithoutTimezone = matcher.group(1); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + long timestampMs = format.parse(timestampWithoutTimezone).getTime(); + // Parse the timezone. + String timezone = matcher.group(2); + if ("Z".equals(timezone)) { + // UTC (no offset). } else { - formatPattern = ISO_8601_FORMAT; + long sign = "+".equals(matcher.group(4)) ? 1 : -1; + long hours = Long.parseLong(matcher.group(5)); + String minutesString = matcher.group(7); + long minutes = TextUtils.isEmpty(minutesString) ? 0 : Long.parseLong(minutesString); + long timestampOffsetMs = sign * (((hours * 60) + minutes) * 60 * 1000); + timestampMs -= timestampOffsetMs; } - //parse - try { - SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); - } - - } else { - throw new ParserException("Unable to parse ISO 8601. Input value is null"); + return timestampMs; + } catch (ParseException e) { + throw new ParserException(e); } } } - + } From bc7bfb4e7cccdbdb203e3e3af1a07c32e3b47d51 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 07:29:56 -0800 Subject: [PATCH 108/148] Fix setting supported ad MIME types without preloading ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177810991 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 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 cf8b8a3f6d..00bf0bd493 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 @@ -315,20 +315,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - if (ENABLE_PRELOADING) { - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with preloading"); - } - } else { - adsManager.init(); - if (DEBUG) { - Log.d(TAG, "Initialized without preloading"); - } + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + adsManager.init(adsRenderingSettings); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); adPlaybackState = new AdPlaybackState(adGroupTimesUs); From 13a7037c82a91290757862e5704920e6f89d6392 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 07:35:50 -0800 Subject: [PATCH 109/148] Fix playback of FLV live streams with no audio track Issue: #3188 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177811487 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/flv/FlvExtractor.java | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a55044ad7e..ff0af256cf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,8 @@ ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * Fix handling of playback parameters changes while paused when followed by a seek. +* Fix playback of live FLV streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. * CEA-608: Fix handling of row count changes in roll-up mode diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 2da075ff53..d908f28945 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -78,6 +78,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; @@ -93,6 +94,7 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -134,6 +136,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; bytesToNextTagHeader = 0; } @@ -255,11 +258,11 @@ public final class FlvExtractor implements Extractor { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { - ensureOutputSeekMap(); - audioReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { - ensureOutputSeekMap(); - videoReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); long durationUs = metadataReader.getDurationUs(); @@ -288,11 +291,15 @@ public final class FlvExtractor implements Extractor { return tagData; } - private void ensureOutputSeekMap() { + private void ensureReadyForMediaOutput() { if (!outputSeekMap) { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } + if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } } } From aef9063e946a0ade455b90aa783220a18cc8c47d Mon Sep 17 00:00:00 2001 From: amesbah Date: Mon, 4 Dec 2017 10:50:51 -0800 Subject: [PATCH 110/148] Add @SuppressWarnings("ComparableType") for instances of a class implementing 'Comparable' where T is not compatible with the type of the class. In order to facilitate enabling a compile-time error check, we are suppressing these existing instances. Once the compile-time error is enabled, we will file bugs to clean up any unfixed instances in []. Note that this CL should result in no effective changes to the code, but the code as currently-written might contain a real bug. If you'd prefer to fix the bug now, please either reply with edits, or accept this CL then follow up with a change that fixes the underlying issue. Tested: tap_presubmit: [] Some tests failed; test failures are believed to be unrelated to this CL ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177836122 --- .../exoplayer2/source/hls/playlist/HlsMediaPlaylist.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index b21ecb02d5..1f44607f98 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -29,9 +29,8 @@ import java.util.List; */ public final class HlsMediaPlaylist extends HlsPlaylist { - /** - * Media segment reference. - */ + /** Media segment reference. */ + @SuppressWarnings("ComparableType") public static final class Segment implements Comparable { /** From 82122b9f3bf994ab326be6dd026b6b54f323c527 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 00:29:56 -0800 Subject: [PATCH 111/148] Make one ad request in ImaAdsLoader This fixes an issue where quickly detaching and reattaching the player might cause ads to be requested multiple times with both responses handled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177922167 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 18 ++++++++++++------ 1 file changed, 12 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 00bf0bd493..92ca24c889 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 @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -120,6 +121,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private Object pendingAdRequestContext; private List supportedMimeTypes; private EventListener eventListener; private Player player; @@ -183,10 +185,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; - /** - * Whether {@link #release()} has been called. - */ - private boolean released; /** * Creates a new IMA ads loader. @@ -296,7 +294,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void release() { - released = true; + pendingAdRequestContext = null; if (adsManager != null) { adsManager.destroy(); adsManager = null; @@ -308,10 +306,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (released) { + if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { adsManager.destroy(); return; } + pendingAdRequestContext = null; this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); @@ -403,6 +402,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } @@ -622,10 +622,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. private void requestAds() { + if (pendingAdRequestContext != null) { + // Ad request already in flight. + return; + } + pendingAdRequestContext = new Object(); AdsRequest request = imaSdkFactory.createAdsRequest(); request.setAdTagUrl(adTagUri.toString()); request.setAdDisplayContainer(adDisplayContainer); request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); } From 3c0bb7263b73c0472a5274f545b9be8b4f086cca Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 03:53:01 -0800 Subject: [PATCH 112/148] Invoke onLoadCanceled/Completed for ExtractorMediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177936271 --- .../source/ExtractorMediaPeriod.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index e3c9012dbc..f557d4ac97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -405,14 +405,26 @@ import java.util.Arrays; @Override public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - copyLengthFromLoader(loadable); - loadingFinished = true; if (durationUs == C.TIME_UNSET) { long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); } + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); + copyLengthFromLoader(loadable); + loadingFinished = true; callback.onContinueLoadingRequested(this); } @@ -422,6 +434,18 @@ import java.util.Arrays; if (released) { return; } + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); @@ -434,7 +458,6 @@ import java.util.Arrays; @Override public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - copyLengthFromLoader(loadable); boolean isErrorFatal = isLoadableExceptionFatal(error); eventDispatcher.loadError( loadable.dataSpec, @@ -450,6 +473,7 @@ import java.util.Arrays; loadable.bytesLoaded, error, /* wasCanceled= */ isErrorFatal); + copyLengthFromLoader(loadable); if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } From 9cb0c2f70292c65e0f430263e210e5ea48c11878 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 04:21:18 -0800 Subject: [PATCH 113/148] Remove self @link ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177938212 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 1374b73709..0d724d4fd2 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 @@ -865,15 +865,15 @@ public class SimpleExoPlayer implements ExoPlayer { // Internal methods. /** - * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}. + * Creates the {@link ExoPlayer} implementation used by this instance. * * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @return A new {@link ExoPlayer} instance. */ - protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + protected ExoPlayer createExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return new ExoPlayerImpl(renderers, trackSelector, loadControl); } From 02e32a183881490b581528407965d8de4f6469f6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 05:19:48 -0800 Subject: [PATCH 114/148] Hide subtitles when switching player in SimpleExoPlayerView ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177941993 --- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 3 +++ .../java/com/google/android/exoplayer2/ui/SubtitleView.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 b09e80c591..dcc1c62569 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 @@ -425,6 +425,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (shutterView != null) { shutterView.setVisibility(VISIBLE); } + if (subtitleView != null) { + subtitleView.setCues(null); + } if (player != null) { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 618f2fa336..d89f82b7c4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -87,9 +88,9 @@ public final class SubtitleView extends View implements TextOutput { /** * Sets the cues to be displayed by the view. * - * @param cues The cues to display. + * @param cues The cues to display, or null to clear the cues. */ - public void setCues(List cues) { + public void setCues(@Nullable List cues) { if (this.cues == cues) { return; } From 39e8f07566a61c4d45d5c8f4003f03d5440ca549 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Dec 2017 10:33:23 -0800 Subject: [PATCH 115/148] Add missing Nullable annotation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178117289 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0d724d4fd2..544b10b7ef 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 @@ -726,7 +726,7 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { player.setPlaybackParameters(playbackParameters); } From e7d4524c27820e8bb0b9954b5113364b34eca0a9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 02:45:16 -0800 Subject: [PATCH 116/148] Use mappedTrackInfo local ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178216750 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 7d0975a750..0623f48a51 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 @@ -233,8 +233,8 @@ public class PlayerActivity extends Activity implements OnClickListener, } else if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); + trackSelectionHelper.showSelectionDialog( + this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag()); } } } From a4a02f74498c86e65b95c5961b8c794ef4f53f2a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 03:05:22 -0800 Subject: [PATCH 117/148] Skip ads before the initial player position Issue: #3527 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218391 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 57 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ff0af256cf..a7199301d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,9 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* IMA extension: + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). ### 2.6.0 ### 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 92ca24c889..58268d5670 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 @@ -65,7 +65,6 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { - static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } @@ -132,6 +131,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private AdsManager adsManager; private Timeline timeline; private long contentDurationMs; + private int podIndexOffset; private AdPlaybackState adPlaybackState; // Fields tracking IMA's state. @@ -274,6 +274,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.resume(); } } else { + pendingContentPositionMs = player.getCurrentPosition(); requestAds(); } } @@ -311,19 +312,45 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; + + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + } else if (adGroupIndexForPosition > 0) { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always + // start at 1, so take this into account when offsetting the pod index for the skipped ads. + podIndexOffset = adGroupIndexForPosition - 1; + } + adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); + updateAdPlaybackState(); } @@ -351,13 +378,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. AdPodInfo adPodInfo = ad.getAdPodInfo(); int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex; + adGroupIndex = + podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); int adCountInAdGroup = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " - + adGroupIndex); + Log.d( + TAG, + "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); } adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); updateAdPlaybackState(); @@ -740,4 +769,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } + /** + * Returns the index of the ad group that should be played before playing the content at {@code + * playbackPositionUs} when starting playback for the first time. This is the latest ad group at + * or before the specified playback position. If the first ad is after the playback position, + * returns {@link C#INDEX_UNSET}. + */ + private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) { + for (int i = 0; i < adGroupTimesUs.length; i++) { + long adGroupTimeUs = adGroupTimesUs[i]; + // A postroll ad is after any position in the content. + if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) { + return i == 0 ? C.INDEX_UNSET : (i - 1); + } + } + return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1); + } } From f677d1309d7971d7f7bbdd07e5ca412288ea4c3d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Dec 2017 03:06:54 -0800 Subject: [PATCH 118/148] Blacklist Moto Z from using secure DummySurface. Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218535 --- .../com/google/android/exoplayer2/video/DummySurface.java | 7 +++++-- 1 file changed, 5 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 2d7a9dfd33..cc50443296 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 @@ -150,14 +150,17 @@ public final class DummySurface extends Surface { */ @TargetApi(24) private static boolean enableSecureDummySurfaceV24(Context context) { - if (Util.SDK_INT < 26 && "samsung".equals(Util.MANUFACTURER)) { + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. 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. + // Pre API level 26 devices were not well tested unless they supported VR mode. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); From a99295b364d86d9113addc34ff5ff503343f9d40 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 06:38:39 -0800 Subject: [PATCH 119/148] Fix ad loading when there is no preroll ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178234009 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a7199301d7..e4a0b40b23 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,7 @@ * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. ### 2.6.0 ### 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 58268d5670..0fa34e5144 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 @@ -326,9 +326,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setMimeTypes(supportedMimeTypes); int adGroupIndexForPosition = getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == C.INDEX_UNSET) { + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { pendingContentPositionMs = C.TIME_UNSET; - } else if (adGroupIndexForPosition > 0) { + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { // Skip ad groups before the one at or immediately before the playback position. for (int i = 0; i < adGroupIndexForPosition; i++) { adPlaybackState.playedAdGroup(i); @@ -341,8 +345,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always - // start at 1, so take this into account when offsetting the pod index for the skipped ads. + // midroll/postroll. Midroll pod indices start at 1. podIndexOffset = adGroupIndexForPosition - 1; } From 88d012bad0fbf482629d8a07d8d221fe735c9628 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 04:44:55 -0800 Subject: [PATCH 120/148] Treat captions that are wider than expected as middle aligned Issue: #3534 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178364353 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 0483f909b3..f018e055fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -740,8 +740,10 @@ public final class Cea608Decoder extends CeaDecoder { // The number of empty columns after the end of the text, in the same range. int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int startEndPaddingDelta = startPadding - endPadding; - if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { - // Treat approximately centered pop-on captions are middle aligned. + if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) { + // Treat approximately centered pop-on captions as middle aligned. We also treat captions + // that are wider than they should be in this way. See + // https://github.com/google/ExoPlayer/issues/3534. position = 0.5f; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { From 3e9c86fcb5826fc3fab0c94bfae317f50ecb0477 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 06:51:51 -0800 Subject: [PATCH 121/148] Add an option to turn off hiding controls during ads Issue: #3532 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178372763 --- RELEASENOTES.md | 2 + .../exoplayer2/ui/SimpleExoPlayerView.java | 122 ++++++++++-------- library/ui/src/main/res/values/attrs.xml | 1 + 3 files changed, 70 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e4a0b40b23..c702a22ce6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,8 @@ * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). ### 2.6.0 ### 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 dcc1c62569..1f67b83ba0 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 @@ -56,146 +56,144 @@ import java.util.List; /** * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - *

    - * A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

    A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. * *

    Attributes

    + * * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: + * *

    + * *

      *
    • {@code use_artwork} - Whether artwork is used if available in audio streams. *
        - *
      • Corresponding method: {@link #setUseArtwork(boolean)}
      • - *
      • Default: {@code true}
      • + *
      • Corresponding method: {@link #setUseArtwork(boolean)} + *
      • Default: {@code true} *
      - *
    • *
    • {@code default_artwork} - Default artwork to use if no artwork available in audio * streams. *
        - *
      • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
      • - *
      • Default: {@code null}
      • + *
      • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
      • Default: {@code null} *
      - *
    • *
    • {@code use_controller} - Whether the playback controls can be shown. *
        - *
      • Corresponding method: {@link #setUseController(boolean)}
      • - *
      • Default: {@code true}
      • + *
      • Corresponding method: {@link #setUseController(boolean)} + *
      • Default: {@code true} *
      - *
    • *
    • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. *
        - *
      • Corresponding method: {@link #setControllerHideOnTouch(boolean)}
      • - *
      • Default: {@code true}
      • + *
      • Corresponding method: {@link #setControllerHideOnTouch(boolean)} + *
      • Default: {@code true} *
      - *
    • *
    • {@code auto_show} - Whether the playback controls are automatically shown when * playback starts, pauses, ends, or fails. If set to false, the playback controls can be * manually operated with {@link #showController()} and {@link #hideController()}. *
        - *
      • Corresponding method: {@link #setControllerAutoShow(boolean)}
      • - *
      • Default: {@code true}
      • + *
      • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
      • Default: {@code true} + *
      + *
    • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
        + *
      • Corresponding method: {@link #setControllerHideDuringAds(boolean)} + *
      • Default: {@code true} *
      - *
    • *
    • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. *
        - *
      • Corresponding method: {@link #setResizeMode(int)}
      • - *
      • Default: {@code fit}
      • + *
      • Corresponding method: {@link #setResizeMode(int)} + *
      • Default: {@code fit} *
      - *
    • *
    • {@code surface_type} - The type of surface view used for video playbacks. Valid * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} * is recommended for audio only applications, since creating the surface can be expensive. * Using {@code surface_view} is recommended for video applications. *
        - *
      • Corresponding method: None
      • - *
      • Default: {@code surface_view}
      • + *
      • Corresponding method: None + *
      • Default: {@code surface_view} *
      - *
    • *
    • {@code shutter_background_color} - The background color of the {@code exo_shutter} * view. *
        - *
      • Corresponding method: {@link #setShutterBackgroundColor(int)}
      • - *
      • Default: {@code unset}
      • + *
      • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
      • Default: {@code unset} *
      - *
    • *
    • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
        - *
      • Corresponding method: None
      • - *
      • Default: {@code R.id.exo_simple_player_view}
      • + *
      • Corresponding method: None + *
      • Default: {@code R.id.exo_simple_player_view} *
      *
    • {@code controller_layout_id} - Specifies the id of the layout resource to be * inflated by the child {@link PlaybackControlView}. See below for more details. *
        - *
      • Corresponding method: None
      • - *
      • Default: {@code R.id.exo_playback_control_view}
      • + *
      • Corresponding method: None + *
      • Default: {@code R.id.exo_playback_control_view} *
      *
    • All attributes that can be set on a {@link PlaybackControlView} can also be set on a * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
    • *
    * *

    Overriding the layout file

    + * * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain * configurations, you can define {@code exo_simple_player_view.xml} layout files in your * application {@code res/layout*} directories. These layouts will override the one provided by the * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and * binds its children by looking for the following ids: + * *

    + * *

      *
    • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video * or album art of the media being played, and the configured {@code resize_mode}. The video * surface view is inflated into this frame as its first child. *
        - *
      • Type: {@link AspectRatioFrameLayout}
      • + *
      • Type: {@link AspectRatioFrameLayout} *
      - *
    • *
    • {@code exo_shutter} - A view that's made visible when video should be hidden. This * view is typically an opaque view that covers the video surface view, thereby obscuring it * when visible. *
        - *
      • Type: {@link View}
      • + *
      • Type: {@link View} *
      - *
    • *
    • {@code exo_subtitles} - Displays subtitles. *
        - *
      • Type: {@link SubtitleView}
      • + *
      • Type: {@link SubtitleView} *
      - *
    • *
    • {@code exo_artwork} - Displays album art. *
        - *
      • Type: {@link ImageView}
      • + *
      • Type: {@link ImageView} *
      - *
    • *
    • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
        - *
      • Type: {@link View}
      • + *
      • Type: {@link View} *
      - *
    • *
    • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as - * {@code rewind_increment} will not be automatically propagated through to this instance. If - * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. *
        - *
      • Type: {@link PlaybackControlView}
      • + *
      • Type: {@link PlaybackControlView} *
      - *
    • *
    • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
        - *
      • Type: {@link FrameLayout}
      • + *
      • Type: {@link FrameLayout} *
      - *
    • *
    - *

    - * All child views are optional and so can be omitted if not required, however where defined they + * + *

    All child views are optional and so can be omitted if not required, however where defined they * must be of the expected type. * *

    Specifying a custom layout file

    + * * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a * single instance in a layout file. This is achieved by setting the {@code player_layout_id} @@ -224,6 +222,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private Bitmap defaultArtwork; private int controllerShowTimeoutMs; private boolean controllerAutoShow; + private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { @@ -267,6 +266,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; boolean controllerHideOnTouch = true; boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -288,6 +288,8 @@ public final class SimpleExoPlayerView extends FrameLayout { controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); } finally { a.recycle(); } @@ -358,6 +360,7 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.controllerHideOnTouch = controllerHideOnTouch; this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; this.useController = useController && controller != null; hideController(); } @@ -649,6 +652,16 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerAutoShow = controllerAutoShow; } + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -784,8 +797,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { - if (isPlayingAd()) { - // Never show the controller if an ad is currently playing. + if (isPlayingAd() && controllerHideDuringAds) { return; } if (useController) { @@ -956,7 +968,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } else { maybeShowController(false); @@ -965,7 +977,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } } diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 525f95768c..1ab3854d21 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -47,6 +47,7 @@ + From a0d42b53b734ec8d533ef56681ffad9453903d59 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 07:57:06 -0800 Subject: [PATCH 122/148] Make DashMediaSource.Builder a factory for DashMediaSources This is in preparation for supporting non-extractor MediaSources for ads in AdsMediaSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178377627 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../exoplayer2/source/ads/AdsMediaSource.java | 65 +++--- .../source/dash/DashMediaSource.java | 192 ++++++++++-------- .../playbacktests/gts/DashTestRunner.java | 5 +- 4 files changed, 141 insertions(+), 130 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 0623f48a51..1be6df8437 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 @@ -371,11 +371,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_DASH: - return DashMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: return HlsMediaSource.Builder .forDataSource(uri, mediaDataSourceFactory) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 54a8fd96ae..c701d6ca64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -44,6 +44,31 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { + /** Factory for creating {@link MediaSource}s to play ad media. */ + public interface MediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the media or manifest to play. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. + * @return The new media source. + */ + MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link + * C#TYPE_OTHER}. + * + * @return The content types supported by media sources created by this factory. + */ + int[] getSupportedTypes(); + } + /** Listener for ads media source events. */ public interface EventListener extends MediaSourceEventListener { @@ -77,7 +102,7 @@ public final class AdsMediaSource implements MediaSource { @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final AdMediaSourceFactory adMediaSourceFactory; + private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -138,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -186,7 +211,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); + adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -371,44 +396,16 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Factory for {@link MediaSource}s for loading ad media. - */ - private interface AdMediaSourceFactory { - - /** - * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. - * - * @param uri The URI of the ad. - * @param handler A handler for listener events. May be null if delivery of events is not - * required. - * @param listener A listener for events. May be null if delivery of events is not required. - * @return The new media source. - */ - MediaSource createAdMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); - - /** - * Returns the content types supported by media sources created by this factory. Each element - * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or - * {@link C#TYPE_OTHER}. - * - * @return The content types supported by the factory. - */ - int[] getSupportedTypes(); - - } - - private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } @Override - public MediaSource createAdMediaSource( + public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { return new ExtractorMediaSource.Builder(uri, dataSourceFactory) .setEventListener(handler, listener) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 22bc2b08ec..e2a6097411 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; @@ -61,59 +62,31 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * Builder for {@link DashMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link DashMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final DashManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; - + private @Nullable ParsingLoadable.Parser manifestParser; private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link DashMediaSource}s. * - * @param manifest The manifest. {@link DashManifest#dynamic} must be false. * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideloadedManifest(DashManifest manifest, - DashChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.dynamic); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(DashManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + DashChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; } @@ -123,76 +96,119 @@ public final class DashMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. - * @return This builder. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is - * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser( + public Factory setManifestParser( ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** - * Builds a new {@link DashMediaSource} using the current parameters. - *

    - * After this call, the builder should not be re-used. + * Returns a new {@link DashMediaSource} using the current parameters and the specified + * sideloaded manifest. * - * @return The newly built {@link DashMediaSource}. + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. */ - public DashMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { - manifestParser = new DashManifestParser(); - } - return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + public DashMediaSource createMediaSource( + DashManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.dynamic); + isCreateCalled = true; + return new DashMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, eventListener); } + /** + * Returns a new {@link DashMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link DashMediaSource}. + */ + public DashMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link DashMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + */ + @Override + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH}; + } } /** @@ -259,7 +275,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, @@ -276,7 +292,7 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, @@ -295,7 +311,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -321,7 +337,7 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -347,7 +363,7 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 215d8a0518..8973853245 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,11 +316,10 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return DashMediaSource.Builder - .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + return new DashMediaSource.Factory(chunkSourceFactory, manifestDataSourceFactory) .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) .setLivePresentationDelayMs(0) - .build(); + .createMediaSource(manifestUri); } @Override From 4c71d6361ddb322f53aca12be0f333a8e287abfe Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 08:32:46 -0800 Subject: [PATCH 123/148] Make SsMediaSource.Builder a factory for SsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178380856 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../source/smoothstreaming/SsMediaSource.java | 183 ++++++++++-------- 2 files changed, 104 insertions(+), 88 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 1be6df8437..38938bd367 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 @@ -365,11 +365,10 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return SsMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 34343e08e3..eb6ceb3dcc 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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; @@ -52,59 +53,31 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } - /** - * Builder for {@link SsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SsMediaSource}. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final SsManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; - + private @Nullable ParsingLoadable.Parser manifestParser; private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link SsMediaSource}s. * - * @param manifest The manifest. {@link SsManifest#isLive} must be false. * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideLoadedManifest(SsManifest manifest, - SsChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.isLive); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(SsManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + SsChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; } @@ -114,73 +87,117 @@ public final class SsMediaSource implements MediaSource, * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the * default start position should precede the end of the live window. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is an instance of - * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + public Factory setManifestParser(ParsingLoadable.Parser manifestParser) { + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** - * Builds a new {@link SsMediaSource} using the current parameters. - *

    - * After this call, the builder should not be re-used. + * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded + * manifest. * - * @return The newly built {@link SsMediaSource}. + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + * @throws IllegalArgumentException If {@link SsManifest#isLive} is true. */ - public SsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { + public SsMediaSource createMediaSource( + SsManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.isLive); + isCreateCalled = true; + return new SsMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link SsMediaSource}. + */ + public SsMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + */ + @Override + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { manifestParser = new SsManifestParser(); } - return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + return new SsMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, eventListener); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_SS}; + } + } /** @@ -228,7 +245,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -245,7 +262,7 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -264,7 +281,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -288,7 +305,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -312,7 +329,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, From 42b612a49f801f95bfa72b81cafef90329ac2737 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 09:07:55 -0800 Subject: [PATCH 124/148] Make HlsMediaSource.Builder a factory for HlsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178384204 --- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/source/hls/HlsMediaSource.java | 151 +++++++++--------- 2 files changed, 75 insertions(+), 82 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 38938bd367..215c4708e8 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 @@ -375,10 +375,8 @@ public class PlayerActivity extends Activity implements OnClickListener, buildDataSourceFactory(false)) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: - return HlsMediaSource.Builder - .forDataSource(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) .setEventListener(mainHandler, eventLogger) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 4e904032fd..3a55cb8a17 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -26,6 +27,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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; @@ -47,66 +49,51 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } - /** - * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link HlsMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri manifestUri; private final HlsDataSourceFactory hlsDataSourceFactory; private HlsExtractorFactory extractorFactory; - private ParsingLoadable.Parser playlistParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser playlistParser; private int minLoadableRetryCount; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link DataSource.Factory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory A data source factory that will be wrapped by a - * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, - * segments and keys. - * @return A new builder. + * @param dataSourceFactory A data source factory that will be wrapped by a {@link + * DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and + * keys. */ - public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { - return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultHlsDataSourceFactory(dataSourceFactory)); } /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link HlsDataSourceFactory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for * manifests, segments and keys. - * @return A new builder. */ - public static Builder forHlsDataSource(Uri manifestUri, - HlsDataSourceFactory dataSourceFactory) { - return new Builder(manifestUri, dataSourceFactory); - } - - private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { - this.manifestUri = manifestUri; - this.hlsDataSourceFactory = hlsDataSourceFactory; - + public Factory(HlsDataSourceFactory hlsDataSourceFactory) { + this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory); + extractorFactory = HlsExtractorFactory.DEFAULT; minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } /** - * Sets the factory for {@link Extractor}s for the segments. Default value is - * {@link HlsExtractorFactory#DEFAULT}. + * Sets the factory for {@link Extractor}s for the segments. The default value is {@link + * HlsExtractorFactory#DEFAULT}. * * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the - * segments. - * @return This builder. + * segments. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { - this.extractorFactory = extractorFactory; + public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorFactory = Assertions.checkNotNull(extractorFactory); return this; } @@ -114,63 +101,71 @@ public final class HlsMediaSource implements MediaSource, * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. - * @return This builder. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, - MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the parser to parse HLS playlists. The default is an instance of - * {@link HlsPlaylistParser}. + * Sets the parser to parse HLS playlists. The default is an instance of {@link + * HlsPlaylistParser}. * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { - this.playlistParser = playlistParser; + public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { + Assertions.checkState(!isCreateCalled); + this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } /** - * Builds a new {@link HlsMediaSource} using the current parameters. - *

    - * After this call, the builder should not be re-used. + * Returns a new {@link HlsMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @return The newly built {@link HlsMediaSource}. + * @return The new {@link HlsMediaSource}. */ - public HlsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - if (extractorFactory == null) { - extractorFactory = HlsExtractorFactory.DEFAULT; - } + public MediaSource createMediaSource(Uri playlistUri) { + return createMediaSource(playlistUri, null, null); + } + + /** + * Returns a new {@link HlsMediaSource} using the current parameters. + * + * @param playlistUri The playlist {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link HlsMediaSource}. + */ + @Override + public MediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } - return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - minLoadableRetryCount, eventHandler, eventListener, playlistParser); + return new HlsMediaSource( + playlistUri, + hlsDataSourceFactory, + extractorFactory, + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_HLS}; + } } /** @@ -195,7 +190,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, @@ -213,7 +208,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, @@ -234,7 +229,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, From babd5750e3411cf78ea597dbbe92f08ecdd54b59 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:08:01 -0800 Subject: [PATCH 125/148] Use surfaceless context in DummySurface, if available Issue: #3558 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178604607 --- RELEASENOTES.md | 2 + .../exoplayer2/video/DummySurface.java | 108 ++++++++++-------- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c702a22ce6..01c91995a7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Use surfaceless context for secure DummySurface, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). 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 cc50443296..2c172c086b 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 @@ -24,6 +24,7 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; +import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -56,10 +57,13 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; +import android.support.annotation.IntDef; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import javax.microedition.khronos.egl.EGL10; /** @@ -70,16 +74,27 @@ public final class DummySurface extends Surface { private static final String TAG = "DummySurface"; + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - private static boolean secureSupported; - private static boolean secureSupportedInitialized; + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + private @interface SecureMode {} + + private static final int SECURE_MODE_NONE = 0; + private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + private static final int SECURE_MODE_PROTECTED_PBUFFER = 2; /** * Whether the surface is secure. */ public final boolean secure; + private static @SecureMode int secureMode; + private static boolean secureModeInitialized; + private final DummySurfaceThread thread; private boolean threadReleased; @@ -90,11 +105,11 @@ public final class DummySurface extends Surface { * @return Whether the device supports secure dummy surfaces. */ public static synchronized boolean isSecureSupported(Context context) { - if (!secureSupportedInitialized) { - secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); - secureSupportedInitialized = true; + if (!secureModeInitialized) { + secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); + secureModeInitialized = true; } - return secureSupported; + return secureMode != SECURE_MODE_NONE; } /** @@ -113,7 +128,7 @@ public final class DummySurface extends Surface { assertApiLevel17OrHigher(); Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); - return thread.init(secure); + return thread.init(secure ? secureMode : SECURE_MODE_NONE); } private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { @@ -143,33 +158,34 @@ public final class DummySurface extends Surface { } } - /** - * Returns whether use of secure dummy surfaces should be enabled. - * - * @param context Any {@link Context}. - */ @TargetApi(24) - private static boolean enableSecureDummySurfaceV24(Context context) { + private static @SecureMode int getSecureModeV24(Context context) { if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. // Moto Z XT1650 is also affected. See // https://github.com/google/ExoPlayer/issues/3215. - return false; + return SECURE_MODE_NONE; } 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. See - // https://github.com/google/ExoPlayer/issues/3215. - return false; + // Pre API level 26 devices were not well tested unless they supported VR mode. + return SECURE_MODE_NONE; } 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 (eglExtensions == null) { + return SECURE_MODE_NONE; } - return true; + if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) { + return SECURE_MODE_NONE; + } + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may + // require support for EXT_protected_surface, but in practice it works on some devices that + // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558. + return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT) + ? SECURE_MODE_SURFACELESS_CONTEXT + : SECURE_MODE_PROTECTED_PBUFFER; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, @@ -195,12 +211,12 @@ public final class DummySurface extends Surface { textureIdHolder = new int[1]; } - public DummySurface init(boolean secure) { + public DummySurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), this); boolean wasInterrupted = false; synchronized (this) { - handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget(); + handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); while (surface == null && initException == null && initError == null) { try { wait(); @@ -236,7 +252,7 @@ public final class DummySurface extends Surface { switch (msg.what) { case MSG_INIT: try { - initInternal(msg.arg1 != 0); + initInternal(/* secureMode= */ msg.arg1); } catch (RuntimeException e) { Log.e(TAG, "Failed to initialize dummy surface", e); initException = e; @@ -266,7 +282,7 @@ public final class DummySurface extends Surface { } } - private void initInternal(boolean secure) { + private void initInternal(@SecureMode int secureMode) { display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); @@ -294,43 +310,45 @@ public final class DummySurface extends Surface { EGLConfig config = configs[0]; int[] glAttributes; - if (secure) { + if (secureMode == SECURE_MODE_NONE) { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE}; } else { - glAttributes = new int[] { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE}; + glAttributes = + new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; } context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); Assertions.checkState(context != null, "eglCreateContext failed"); - int[] pbufferAttributes; - if (secure) { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, - EGL_NONE}; + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL_NO_SURFACE; } else { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_NONE}; + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; + } else { + pbufferAttributes = new int[] {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + } + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + surface = pbuffer; } - pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); - boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); + boolean eglMadeCurrent = eglMakeCurrent(display, surface, surface, context); Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); glGenTextures(1, textureIdHolder, 0); surfaceTexture = new SurfaceTexture(textureIdHolder[0]); surfaceTexture.setOnFrameAvailableListener(this); - surface = new DummySurface(this, surfaceTexture, secure); + this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE); } private void releaseInternal() { From f31696b79a608328454f1ae20ef8801aba91dfd0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:23:00 -0800 Subject: [PATCH 126/148] Make ExtractorMediaSource.Builder a factory for ExtractorMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178605481 --- .../exoplayer2/imademo/PlayerManager.java | 3 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 185 +++++++++--------- .../exoplayer2/source/ads/AdsMediaSource.java | 26 +-- 7 files changed, 117 insertions(+), 132 deletions(-) diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 6b840830c5..ec21f6d265 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -70,7 +70,8 @@ import com.google.android.exoplayer2.util.Util; // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); + new ExtractorMediaSource.Factory(dataSourceFactory) + .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, 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 215c4708e8..a60ae0c876 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 @@ -378,9 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: - return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); default: { throw new IllegalStateException("Unsupported type: " + type); } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index bd6e698dc6..fd18a3b1ae 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index aa61df74d9..d3ab421655 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 746f3d273f..3cc1a1d340 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -105,10 +106,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 247eacd519..e787c34a9c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -103,129 +104,113 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; - /** - * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link ExtractorMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private ExtractorsFactory extractorsFactory; + private @Nullable ExtractorsFactory extractorsFactory; + private @Nullable String customCacheKey; private int minLoadableRetryCount; - @Nullable private Handler eventHandler; - @Nullable private MediaSourceEventListener eventListener; - @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a new factory for {@link ExtractorMediaSource}s. + * * @param dataSourceFactory A factory for {@link DataSource}s to read the media. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory) { - this.uri = uri; + public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the factory for {@link Extractor}s to process the media stream. Default value is an - * instance of {@link DefaultExtractorsFactory}. - * - * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the - * possible formats are known, pass a factory that instantiates extractors for those - * formats. - * @return This builder. - */ - public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { - this.extractorsFactory = extractorsFactory; - return this; - } - - /** - * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. - * Default value is null. - * - * @param customCacheKey A custom key that uniquely identifies the original stream. Used for - * cache indexing. - * @return This builder. - */ - public Builder setCustomCacheKey(String customCacheKey) { - this.customCacheKey = customCacheKey; - return this; - } - - /** - * Sets the number of bytes that should be loaded between each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value - * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. * * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between - * each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @return This builder. + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; return this; } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. + * @param uri The {@link Uri}. + * @return The new {@link ExtractorMediaSource}. */ - @Deprecated - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); - return this; + public MediaSource createMediaSource(Uri uri) { + return createMediaSource(uri, null, null); } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. * + * @param uri The {@link Uri}. * @param eventHandler A handler for events. * @param eventListener A listener of events. - * @return This builder. + * @return The new {@link ExtractorMediaSource}. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Builds a new {@link ExtractorMediaSource} using the current parameters. - *

    - * After this call, the builder should not be re-used. - * - * @return The newly built {@link ExtractorMediaSource}. - */ - public ExtractorMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (extractorsFactory == null) { extractorsFactory = new DefaultExtractorsFactory(); } @@ -234,6 +219,10 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe continueLoadingCheckIntervalBytes); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } } /** @@ -244,11 +233,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. 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. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -262,11 +255,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener, String customCacheKey) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); @@ -285,12 +282,18 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + String customCacheKey, + int continueLoadingCheckIntervalBytes) { this( uri, dataSourceFactory, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index c701d6ca64..5611bedcca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -163,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -396,28 +396,4 @@ public final class AdsMediaSource implements MediaSource { } - private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { - - private final DataSource.Factory dataSourceFactory; - - public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { - this.dataSourceFactory = dataSourceFactory; - } - - @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory) - .setEventListener(handler, listener) - .build(); - } - - @Override - public int[] getSupportedTypes() { - // Only ExtractorMediaSource is supported. - return new int[] {C.TYPE_OTHER}; - } - - } - } From 7a089c3a293e32642e8c81f4ba6dbb1749507619 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 07:19:53 -0800 Subject: [PATCH 127/148] Support non-extractor ads in AdsMediaSource and demo apps Issue: #3302 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178615074 --- RELEASENOTES.md | 2 + demos/ima/build.gradle | 2 + .../exoplayer2/imademo/PlayerManager.java | 62 +++++++++++++++++-- .../exoplayer2/demo/PlayerActivity.java | 44 +++++++++---- .../exoplayer2/source/ads/AdsMediaSource.java | 58 ++++++++++++----- 5 files changed, 136 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 01c91995a7..dfc6e3d087 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ * Use surfaceless context for secure DummySurface, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index c32228de28..536d8d4662 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -43,5 +43,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index ec21f6d265..51959451d1 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -17,13 +17,21 @@ package com.google.android.exoplayer2.imademo; import android.content.Context; import android.net.Uri; +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -35,12 +43,12 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; -/** - * Manages the {@link ExoPlayer}, the IMA plugin and all video playback. - */ -/* package */ final class PlayerManager { +/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ +/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { private final ImaAdsLoader adsLoader; + private final DataSource.Factory manifestDataSourceFactory; + private final DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; private long contentPosition; @@ -48,6 +56,14 @@ import com.google.android.exoplayer2.util.Util; public PlayerManager(Context context) { String adTag = context.getString(R.string.ad_tag_url); adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + manifestDataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, context.getString(R.string.application_name))); + mediaDataSourceFactory = + new DefaultDataSourceFactory( + context, + Util.getUserAgent(context, context.getString(R.string.application_name)), + new DefaultBandwidthMeter()); } public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { @@ -74,8 +90,14 @@ import com.google.android.exoplayer2.util.Util; .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. - MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, - adsLoader, simpleExoPlayerView.getOverlayFrameLayout()); + MediaSource mediaSourceWithAds = + new AdsMediaSource( + contentMediaSource, + /* adMediaSourceFactory= */ this, + adsLoader, + simpleExoPlayerView.getOverlayFrameLayout(), + /* eventHandler= */ null, + /* eventListener= */ null); // Prepare the player with the source. player.seekTo(contentPosition); @@ -99,4 +121,32 @@ import com.google.android.exoplayer2.util.Util; adsLoader.release(); } + // AdsMediaSource.MediaSourceFactory implementation. + + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + @ContentType int type = Util.inferContentType(uri); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } } 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 a60ae0c876..fa3c7d401a 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 @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -332,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener, } MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger); } MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -360,26 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener, updateButtonVisibilities(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + private MediaSource buildMediaSource( + Uri uri, + String overrideExtension, + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener) { @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); default: { throw new IllegalStateException("Unsupported type: " + type); } @@ -466,8 +472,22 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, - mainHandler, eventLogger); + AdsMediaSource.MediaSourceFactory adMediaSourceFactory = + new AdsMediaSource.MediaSourceFactory() { + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return PlayerActivity.this.buildMediaSource( + uri, /* overrideExtension= */ null, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource( + mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); } private void releaseAdsLoader() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 5611bedcca..0980e9d011 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -96,13 +96,13 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; + private final MediaSourceFactory adMediaSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; @Nullable private final Handler eventHandler; @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -119,28 +119,31 @@ public final class AdsMediaSource implements MediaSource { private MediaSource.Listener listener; /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

    - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. * @param adsLoader The loader for ads. * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup) { - this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null); + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup) { + this( + contentMediaSource, + dataSourceFactory, + adsLoader, + adUiViewGroup, + /* eventHandler= */ null, + /* eventListener= */ null); } /** * Constructs a new source that inserts ads linearly with the content specified by {@code - * contentMediaSource}. - * - *

    Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -156,14 +159,41 @@ public final class AdsMediaSource implements MediaSource { ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable EventListener eventListener) { + this( + contentMediaSource, + new ExtractorMediaSource.Factory(dataSourceFactory), + adsLoader, + adUiViewGroup, + eventHandler, + eventListener); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. 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. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; + this.adMediaSourceFactory = adMediaSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; From b223988e30c9f176de8a156c9ed49e5cef124b80 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 Dec 2017 09:03:15 -0800 Subject: [PATCH 128/148] Add Builder for ImaAdsLoader and allow early requestAds Also fix propagation of ad errors that occur when no player is attached. Issue: #3548 Issue: #3556 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178767997 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 259 +++++++++++++----- 2 files changed, 195 insertions(+), 68 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dfc6e3d087..1dab497cf2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,10 @@ * Fix ad loading when there is no preroll. * Add an option to turn off hiding controls during ad playback ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). ### 2.6.0 ### 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 0fa34e5144..b4bb886175 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 @@ -19,6 +19,7 @@ import android.content.Context; import android.net.Uri; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.ViewGroup; import android.webkit.WebView; @@ -65,10 +66,80 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { + static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } + /** Builder for {@link ImaAdsLoader}. */ + public static final class Builder { + + private final Context context; + + private @Nullable ImaSdkSettings imaSdkSettings; + private long vastLoadTimeoutMs; + + /** + * Creates a new builder for {@link ImaAdsLoader}. + * + * @param context The context; + */ + public Builder(Context context) { + this.context = Assertions.checkNotNull(context); + vastLoadTimeoutMs = C.TIME_UNSET; + } + + /** + * Sets the IMA SDK settings. The provided settings instance's player type and version fields + * may be overwritten. + * + *

    If this method is not called the default settings will be used. + * + * @param imaSdkSettings The {@link ImaSdkSettings}. + * @return This builder, for convenience. + */ + public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { + this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); + return this; + } + + /** + * Sets the VAST load timeout, in milliseconds. + * + * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. + * @return This builder, for convenience. + * @see AdsRequest#setVastLoadTimeout(float) + */ + public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) { + Assertions.checkArgument(vastLoadTimeoutMs >= 0); + this.vastLoadTimeoutMs = vastLoadTimeoutMs; + return this; + } + + /** + * Returns a new {@link ImaAdsLoader} for the specified ad tag. + * + * @param adTagUri The URI of a compatible ad tag to load. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * information on compatible ad tags. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdTag(Uri adTagUri) { + return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs); + } + + /** + * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. + * + * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of + * making a request via an ad tag URL. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdsResponse(String adsResponse) { + return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs); + } + } + private static final boolean DEBUG = false; private static final String TAG = "ImaAdsLoader"; @@ -94,9 +165,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; - /** - * The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}. - */ + /** The state of ad playback. */ @Retention(RetentionPolicy.SOURCE) @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) private @interface ImaAdState {} @@ -113,7 +182,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private static final int IMA_AD_STATE_PAUSED = 2; - private final Uri adTagUri; + private final @Nullable Uri adTagUri; + private final @Nullable String adsResponse; + private final long vastLoadTimeoutMs; private final Timeline.Period period; private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; @@ -129,6 +200,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private VideoProgressUpdate lastAdProgress; private AdsManager adsManager; + private AdErrorEvent pendingAdErrorEvent; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -144,9 +216,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; - /** - * The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}. - */ + /** The current ad playback state. */ private @ImaAdState int imaAdState; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been @@ -189,13 +259,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** * Creates a new IMA ads loader. * + *

    If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. + * * @param context The context. * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for * more information. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this(context, adTagUri, null); + this(context, adTagUri, null, null, C.TIME_UNSET); } /** @@ -207,9 +279,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * more information. * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * use the default settings. If set, the player type and version fields may be overwritten. + * @deprecated Use {@link ImaAdsLoader.Builder}. */ + @Deprecated public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { + this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET); + } + + private ImaAdsLoader( + Context context, + @Nullable Uri adTagUri, + @Nullable ImaSdkSettings imaSdkSettings, + @Nullable String adsResponse, + long vastLoadTimeoutMs) { + Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; + this.adsResponse = adsResponse; + this.vastLoadTimeoutMs = vastLoadTimeoutMs; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); imaSdkFactory = ImaSdkFactory.getInstance(); @@ -238,6 +324,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * Requests ads, if they have not already been requested. Must be called on the main thread. + * + *

    Ads will be requested automatically when the player is prepared if this method has not been + * called, so it is only necessary to call this method if you want to request ads before preparing + * the player + * + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public void requestAds(ViewGroup adUiViewGroup) { + if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { + // Ads have already been requested. + return; + } + adDisplayContainer.setAdContainer(adUiViewGroup); + pendingAdRequestContext = new Object(); + AdsRequest request = imaSdkFactory.createAdsRequest(); + if (adTagUri != null) { + request.setAdTagUrl(adTagUri.toString()); + } else /* adsResponse != null */ { + request.setAdsResponse(adsResponse); + } + if (vastLoadTimeoutMs != C.TIME_UNSET) { + request.setVastLoadTimeout(vastLoadTimeoutMs); + } + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); + adsLoader.requestAds(request); + } + // AdsLoader implementation. @Override @@ -268,14 +385,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); + maybeNotifyAdError(); if (adPlaybackState != null) { + // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState.copy()); if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } + } else if (adsManager != null) { + // Ads have loaded but the ads manager is not initialized. + startAdPlayback(); } else { - pendingContentPositionMs = player.getCurrentPosition(); - requestAds(); + // Ads haven't loaded yet, so request them. + requestAds(adUiViewGroup); } } @@ -312,49 +434,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; - - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); - this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - int adGroupIndexForPosition = - getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == 0) { - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - pendingContentPositionMs = C.TIME_UNSET; - // There is no preroll and midroll pod indices start at 1. - podIndexOffset = -1; - } else /* adGroupIndexForPosition > 0 */ { - // Skip ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState.playedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - - // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. Midroll pod indices start at 1. - podIndexOffset = adGroupIndexForPosition - 1; + if (player != null) { + // If a player is attached already, start playback immediately. + startAdPlayback(); } - - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - - updateAdPlaybackState(); } // AdEvent.AdEventListener implementation. @@ -384,14 +470,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); - int adCountInAdGroup = adPodInfo.getTotalAds(); + int adCount = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d( - TAG, - "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } - adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); + adPlaybackState.setAdCount(adGroupIndex, adCount); updateAdPlaybackState(); break; case CONTENT_PAUSE_REQUESTED: @@ -434,14 +518,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } - if (eventListener != null) { - IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); - eventListener.onLoadError(exception); + if (pendingAdErrorEvent == null) { + pendingAdErrorEvent = adErrorEvent; } + maybeNotifyAdError(); } // ContentProgressProvider implementation. @@ -653,18 +738,56 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. - private void requestAds() { - if (pendingAdRequestContext != null) { - // Ad request already in flight. - return; + private void startAdPlayback() { + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + + // Set up the ad playback state, skipping ads based on the start position as required. + pendingContentPositionMs = player.getCurrentPosition(); + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. Midroll pod indices start at 1. + podIndexOffset = adGroupIndexForPosition - 1; + } + + // Start ad playback. + adsManager.init(adsRenderingSettings); + updateAdPlaybackState(); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); + } + } + + private void maybeNotifyAdError() { + if (eventListener != null && pendingAdErrorEvent != null) { + IOException exception = + new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()); + eventListener.onLoadError(exception); + pendingAdErrorEvent = null; } - pendingAdRequestContext = new Object(); - AdsRequest request = imaSdkFactory.createAdsRequest(); - request.setAdTagUrl(adTagUri.toString()); - request.setAdDisplayContainer(adDisplayContainer); - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); } private void updateImaStateForPlayerState() { From bf9a919005a0968909f565e5a6840d8eb4b12e08 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Dec 2017 11:05:57 -0800 Subject: [PATCH 129/148] Propagate extras from queue item to metadata item. norelnotes=true ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178785377 --- .../mediasession/MediaSessionConnector.java | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 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 aa007ea1d6..1b1224273f 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -330,6 +331,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; + private final String metadataExtrasPrefix; private final Map commandMap; private Player player; @@ -356,15 +358,15 @@ public final class MediaSessionConnector { /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. - *

    - * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. + * + *

    Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController) { - this(mediaSession, playbackController, true); + public MediaSessionConnector( + MediaSessionCompat mediaSession, PlaybackController playbackController) { + this(mediaSession, playbackController, true, null); } /** @@ -372,17 +374,23 @@ 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, or - * {@code null} if the connector should handle playback actions directly. + * @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). + * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active + * queue item to the session metadata. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController, boolean doMaintainMetadata) { + public MediaSessionConnector( + MediaSessionCompat mediaSession, + PlaybackController playbackController, + boolean doMaintainMetadata, + @Nullable String metadataExtrasPrefix) { this.mediaSession = mediaSession; this.playbackController = playbackController != null ? playbackController : new DefaultPlaybackController(); + this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : ""; this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -553,6 +561,25 @@ public final class MediaSessionConnector { MediaSessionCompat.QueueItem queueItem = queue.get(i); if (queueItem.getQueueId() == activeQueueItemId) { MediaDescriptionCompat description = queueItem.getDescription(); + Bundle extras = description.getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value instanceof String) { + builder.putString(metadataExtrasPrefix + key, (String) value); + } else if (value instanceof CharSequence) { + builder.putText(metadataExtrasPrefix + key, (CharSequence) value); + } else if (value instanceof Long) { + builder.putLong(metadataExtrasPrefix + key, (Long) value); + } else if (value instanceof Integer) { + builder.putLong(metadataExtrasPrefix + key, (Integer) value); + } else if (value instanceof Bitmap) { + builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value); + } else if (value instanceof RatingCompat) { + builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value); + } + } + } if (description.getTitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, String.valueOf(description.getTitle())); From 65ccff24483a60bb93ac31255b062803d9c60f86 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Dec 2017 23:43:16 -0800 Subject: [PATCH 130/148] Update release notes to reflect builder -> factory change ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178866131 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1dab497cf2..61e6d2759d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,7 @@ ### dev-v2 (not yet released) ### -* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, +* Add Factory to ExtractorMediaSource, HlsMediaSource, SsMediaSource, DashMediaSource, SingleSampleMediaSource. * DASH: * Support time zone designators in ISO8601 UTCTiming elements From ae514b68ff3c93e09972d6410ab2b64cffe62684 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 02:16:15 -0800 Subject: [PATCH 131/148] Update release notes for current 2.6.1 feature set ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178877884 --- RELEASENOTES.md | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 61e6d2759d..2f9008045e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,29 +1,11 @@ # Release notes # -### dev-v2 (not yet released) ### +### 2.6.1 ### -* Add Factory to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource, SingleSampleMediaSource. -* DASH: - * Support time zone designators in ISO8601 UTCTiming elements - ([#3524](https://github.com/google/ExoPlayer/issues/3524)). -* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to - use this with `FfmpegAudioRenderer`. -* Support extraction and decoding of Dolby Atmos - ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* DefaultTrackSelector: Support undefined language text track selection when the - preferred language is not available - ([#2980](https://github.com/google/ExoPlayer/issues/2980)). -* Fix handling of playback parameters changes while paused when followed by a - seek. -* Fix playback of live FLV streams that do not contain an audio track - ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. -* CEA-608: Fix handling of row count changes in roll-up mode - ([#3513](https://github.com/google/ExoPlayer/issues/3513)). -* Use surfaceless context for secure DummySurface, if available - ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Support non-ExtractorMediaSource ads ([#3302](https://github.com/google/ExoPlayer/issues/3302)). @@ -36,6 +18,25 @@ ([#3548](https://github.com/google/ExoPlayer/issues/3548)). * Support overriding the ad load timeout ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option + to use this with `FfmpegAudioRenderer`. + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by a + seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### From 8a6c375c53005fec5de6790bb1109f3937363632 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:09:21 -0800 Subject: [PATCH 132/148] Check if native libraries are available in tests. If the library is not available, no tracks can be selected and the tests silently run through by immediately switching to ended state without error. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178904347 --- .../android/exoplayer2/ext/flac/FlacExtractorTest.java | 8 ++++++++ .../android/exoplayer2/ext/flac/FlacPlaybackTest.java | 9 ++++++++- .../android/exoplayer2/ext/opus/OpusPlaybackTest.java | 8 ++++++++ .../android/exoplayer2/ext/vp9/VpxPlaybackTest.java | 9 ++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 7b193997c3..57ce487ac7 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; */ public class FlacExtractorTest extends InstrumentationTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testSample() throws Exception { ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index fd18a3b1ae..b236b706b8 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -37,6 +37,14 @@ public class FlacPlaybackTest extends InstrumentationTestCase { private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_FLAC_URI); } @@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index d3ab421655..c547cff434 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -37,6 +37,14 @@ public class OpusPlaybackTest extends InstrumentationTestCase { private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!OpusLibrary.isAvailable()) { + fail("Opus library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_OPUS_URI); } diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 3cc1a1d340..0a902e2efe 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -43,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase { private static final String TAG = "VpxPlaybackTest"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!VpxLibrary.isAvailable()) { + fail("Vpx library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_URI); } @@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } From 67b94a72a55ba1e1e50665bbede907a0e3fc749e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 08:32:28 -0800 Subject: [PATCH 133/148] Update SingleSampleMediaSource with factory/listener changes - Convert the Builder into a Factory - Have it use MediaSourceEventListener - Also made some misc related fixes to other sources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178906521 --- .../source/ExtractorMediaPeriod.java | 17 +- .../source/ExtractorMediaSource.java | 4 +- .../source/SingleSampleMediaPeriod.java | 99 +++--- .../source/SingleSampleMediaSource.java | 282 +++++++++++++----- .../exoplayer2/source/hls/HlsMediaSource.java | 4 +- 5 files changed, 280 insertions(+), 126 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f557d4ac97..f907dc6229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -431,9 +431,6 @@ import java.util.Arrays; @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - if (released) { - return; - } eventDispatcher.loadCanceled( loadable.dataSpec, C.DATA_TYPE_MEDIA, @@ -446,12 +443,14 @@ import java.util.Arrays; elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded); - copyLengthFromLoader(loadable); - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(); - } - if (enabledTrackCount > 0) { - callback.onContinueLoadingRequested(this); + if (!released) { + copyLengthFromLoader(loadable); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + if (enabledTrackCount > 0) { + callback.onContinueLoadingRequested(this); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index e787c34a9c..3b650482f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -195,7 +195,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param uri The {@link Uri}. * @return The new {@link ExtractorMediaSource}. */ - public MediaSource createMediaSource(Uri uri) { + public ExtractorMediaSource createMediaSource(Uri uri) { return createMediaSource(uri, null, null); } @@ -208,7 +208,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @return The new {@link ExtractorMediaSource}. */ @Override - public MediaSource createMediaSource( + public ExtractorMediaSource createMediaSource( Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { isCreateCalled = true; if (extractorsFactory == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 6101c79b7f..5069a2d633 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -15,13 +15,11 @@ */ package com.google.android.exoplayer2.source; -import android.net.Uri; -import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -43,14 +41,14 @@ import java.util.Arrays; */ private static final int INITIAL_SAMPLE_SIZE = 1024; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; + private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + private final long durationUs; + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; @@ -62,16 +60,20 @@ import java.util.Arrays; /* package */ int sampleSize; private int errorCount; - public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, - int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaPeriod( + DataSpec dataSpec, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + boolean treatLoadErrorsAsEndOfStream) { + this.dataSpec = dataSpec; this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; + this.eventDispatcher = eventDispatcher; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); @@ -125,7 +127,9 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } - loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, + loader.startLoading( + new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), + this, minLoadableRetryCount); return true; } @@ -158,6 +162,18 @@ import java.util.Arrays; @Override public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); sampleSize = loadable.sampleSize; sampleData = loadable.sampleData; loadingFinished = true; @@ -167,34 +183,46 @@ import java.util.Arrays; @Override public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - // Do nothing. + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); } @Override public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - notifyLoadError(error); errorCount++; - if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) { + boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize, + error, + /* wasCanceled= */ cancel); + if (cancel) { loadingFinished = true; return Loader.DONT_RETRY; } return Loader.RETRY; } - // Internal methods. - - private void notifyLoadError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(eventSourceId, e); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private static final int STREAM_STATE_SEND_FORMAT = 0; @@ -259,14 +287,15 @@ import java.util.Arrays; /* package */ static final class SourceLoadable implements Loadable { - private final Uri uri; + public final DataSpec dataSpec; + private final DataSource dataSource; private int sampleSize; private byte[] sampleData; - public SourceLoadable(Uri uri, DataSource dataSource) { - this.uri = uri; + public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { + this.dataSpec = dataSpec; this.dataSource = dataSource; } @@ -286,7 +315,7 @@ import java.util.Arrays; sampleSize = 0; try { // Create and open the input. - dataSource.open(new DataSpec(uri)); + dataSource.open(dataSpec); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 2aa8ccc712..3b0a5a16c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -17,11 +17,14 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource { /** * Listener of {@link SingleSampleMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -45,35 +51,23 @@ public final class SingleSampleMediaSource implements MediaSource { } - /** - * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SingleSampleMediaSource}. */ + public static final class Factory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private final Format format; - private final long durationUs; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private int eventSourceId; private boolean treatLoadErrorsAsEndOfStream; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a factory for {@link SingleSampleMediaSource}s. + * * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will * be obtained. - * @param format The {@link Format} associated with the output track. - * @param durationUs The duration of the media stream in microseconds. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - this.format = format; - this.durationUs = durationUs; + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } @@ -82,37 +76,15 @@ public final class SingleSampleMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } - /** - * Sets the listener to respond to events and the handler to deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. - * - * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. - * @return This builder. - */ - public Builder setEventSourceId(int eventSourceId) { - this.eventSourceId = eventSourceId; - return this; - } - /** * Sets whether load errors will be treated as end-of-stream signal (load errors will not be * propagated). The default value is false. @@ -120,27 +92,53 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated * normally by {@link SampleStream#maybeThrowError()}. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + Assertions.checkState(!isCreateCalled); this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; return this; } /** - * Builds a new {@link SingleSampleMediaSource} using the current parameters. - *

    - * After this call, the builder should not be re-used. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @return The new {@link ExtractorMediaSource}. + */ + public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { + return createMediaSource(uri, format, durationUs, null, null); + } + + /** + * Returns a new {@link SingleSampleMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @param eventHandler A handler for events. + * @param eventListener A listener of events., Format format, long durationUs * @return The newly built {@link SingleSampleMediaSource}. */ - public SingleSampleMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - - return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, - minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + public SingleSampleMediaSource createMediaSource( + Uri uri, + Format format, + long durationUs, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + return new SingleSampleMediaSource( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener, treatLoadErrorsAsEndOfStream); } @@ -151,13 +149,12 @@ public final class SingleSampleMediaSource implements MediaSource { */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final Format format; + private final long durationUs; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; @@ -167,11 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs) { + public SingleSampleMediaSource( + Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } @@ -182,12 +179,16 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount) { + this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false); } /** @@ -203,20 +204,46 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + int eventSourceId, + boolean treatLoadErrorsAsEndOfStream) { + this( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId), + treatLoadErrorsAsEndOfStream); + } + + private SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, + boolean treatLoadErrorsAsEndOfStream) { this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + dataSpec = new DataSpec(uri); timeline = new SinglePeriodTimeline(durationUs, true); } @@ -235,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); + return new SingleSampleMediaPeriod( + dataSpec, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventDispatcher, + treatLoadErrorsAsEndOfStream); } @Override @@ -249,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource { // Do nothing. } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + + private final EventListener eventListener; + private final int eventSourceId; + + public EventListenerWrapper(EventListener eventListener, int eventSourceId) { + this.eventListener = Assertions.checkNotNull(eventListener); + this.eventSourceId = eventSourceId; + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(eventSourceId, error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 3a55cb8a17..8ed202d961 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -131,7 +131,7 @@ public final class HlsMediaSource implements MediaSource, * * @return The new {@link HlsMediaSource}. */ - public MediaSource createMediaSource(Uri playlistUri) { + public HlsMediaSource createMediaSource(Uri playlistUri) { return createMediaSource(playlistUri, null, null); } @@ -144,7 +144,7 @@ public final class HlsMediaSource implements MediaSource, * @return The new {@link HlsMediaSource}. */ @Override - public MediaSource createMediaSource( + public HlsMediaSource createMediaSource( Uri playlistUri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { From 403f773f8703f013b129ef02336b881678f1311f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 08:27:32 -0800 Subject: [PATCH 134/148] Add missing attrs to SimpleExoplayerView They worked without being present in the declare-styleable, but they need to be present for Android Studio auto-complete to suggest them. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179047478 --- library/ui/src/main/res/values/attrs.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 1ab3854d21..b6ed4b17af 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,10 +51,13 @@ + + - + + From b97ce44182ea61fc42584938c79a554c9891654c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:17:30 -0800 Subject: [PATCH 135/148] Pass -1 not C.TIME_UNSET when duration is unknown ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179165479 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 b4bb886175..e0bca20d38 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 @@ -151,6 +151,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ + private static final long IMA_DURATION_UNSET = -1L; + /** * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. @@ -533,6 +536,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public VideoProgressUpdate getContentProgress() { + boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; + long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; if (player == null) { return lastContentProgress; } else if (pendingContentPositionMs != C.TIME_UNSET) { @@ -542,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || contentDurationMs == C.TIME_UNSET) { + } else if (playingAd || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); From 04108eec48f85cc0bf1b5eecadba8b7a5127fdb2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 136/148] Fix condition for detecting that an ad has ended onEnded was being called also for content finishing, as in this case the playing ad index changed (from INDEX_UNSET to 0). Fix this test so we only detect ads finishing. Also add logging for onEnded callbacks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167737 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 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 e0bca20d38..d65bf87605 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 @@ -702,6 +702,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); + } } } @@ -797,16 +800,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; + int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); + playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; if (!sentContentComplete) { - boolean adFinished = (wasPlayingAd && !playingAd) - || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); + boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); + } } if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); @@ -818,7 +825,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } } - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } private void resumeContentInternal() { From 52794e749d1ae214cbb9be1a2f7124ff1dd1e2d0 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 137/148] Fix typo Issue #3594 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167738 --- .../com/google/android/exoplayer2/text/cea/Cea708Decoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 030f0cdbb0..6bdbebc73b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder { private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) - private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) @@ -464,7 +464,7 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF1: case COMMAND_DF2: case COMMAND_DF3: - case COMMAND_DS4: + case COMMAND_DF4: case COMMAND_DF5: case COMMAND_DF6: case COMMAND_DF7: From ab7503843c3c336b5df397a0a97ece2278da168d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 03:06:10 -0800 Subject: [PATCH 138/148] Use playAd/stopAd to control position updates switching Previously the ad/content progress updates were toggled based on whether the player was playing ads or content. After this change, we switch based on whether playAd/stopAd has been called instead. This seems to resolve an issue where occasionally the player would get stuck at the start of an ad, but as I don't have a root cause for that issue and it's only sporadically reproducible I'm not certain this is a reliable fix. Issue: #3525 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179169296 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index d65bf87605..70a8322bba 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 @@ -547,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || !hasContentDuration) { + } else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); @@ -560,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A public VideoProgressUpdate getAdProgress() { if (player == null) { return lastAdProgress; - } else if (!playingAd) { + } else if (imaAdState == IMA_AD_STATE_NONE) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { long adDuration = player.getDuration(); From 236df9a571890d0eed256782e5ed484652b12776 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 08:58:02 -0800 Subject: [PATCH 139/148] Remove DefaultLoadControl buffer time state ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176515168 --- .../android/exoplayer2/DefaultLoadControl.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index d8bc042ad7..a89655c8e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,10 +51,6 @@ public final class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - private static final int ABOVE_HIGH_WATERMARK = 0; - private static final int BETWEEN_WATERMARKS = 1; - private static final int BELOW_LOW_WATERMARK = 2; - private final DefaultAllocator allocator; private final long minBufferUs; @@ -171,11 +167,11 @@ public final class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs) { - int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferTimeState == BELOW_LOW_WATERMARK - || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); + isBuffering = bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering && !targetBufferSizeReached); if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -186,11 +182,6 @@ public final class DefaultLoadControl implements LoadControl { return isBuffering; } - private int getBufferTimeState(long bufferedDurationUs) { - return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK - : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); - } - private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 1e36c76654004a00b9805d9bf6a365334f724c03 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 08:38:02 -0800 Subject: [PATCH 140/148] Allow to configure maximum buffer size in DefaultLoadControl. This adds a parameter to configure a maximum buffer size in bytes. If left at its default of C.LENGTH_UNSET, the target buffer is determined using a overridable method based on the track selection. Also adding a parameter to decide whether to prioritize time or size constraints. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179048554 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultLoadControl.java | 115 +++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2f9008045e..2a2347686d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. * Use surfaceless context for secure `DummySurface`, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * FLV: Fix playback of live streams that do not contain an audio track diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index a89655c8e9..b7b68de7d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,12 +51,23 @@ public final class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + /** + * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control + * automatically determines its target buffer size. + */ + public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET; + + /** The default prioritization of buffer time constraints over size constraints. */ + public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + private final DefaultAllocator allocator; private final long minBufferUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; + private final int targetBufferBytesOverwrite; + private final boolean prioritizeTimeOverSizeThresholds; private final PriorityTaskManager priorityTaskManager; private int targetBufferSize; @@ -75,8 +86,14 @@ public final class DefaultLoadControl implements LoadControl { * @param allocator The {@link DefaultAllocator} used by the loader. */ public DefaultLoadControl(DefaultAllocator allocator) { - this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + this( + allocator, + DEFAULT_MIN_BUFFER_MS, + DEFAULT_MAX_BUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_TARGET_BUFFER_BYTES, + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } /** @@ -92,10 +109,27 @@ public final class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { - this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds) { + this( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, null); } @@ -112,18 +146,30 @@ public final class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. - * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - * periods. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @param priorityTaskManager If not null, registers itself as a task with priority {@link + * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds, PriorityTaskManager priorityTaskManager) { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; + targetBufferBytesOverwrite = targetBufferBytes; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.priorityTaskManager = priorityTaskManager; } @@ -135,12 +181,10 @@ public final class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - targetBufferSize = 0; - for (int i = 0; i < renderers.length; i++) { - if (trackSelections.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); - } - } + targetBufferSize = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? calculateTargetBufferSize(renderers, trackSelections) + : targetBufferBytesOverwrite; allocator.setTargetBufferSize(targetBufferSize); } @@ -162,16 +206,28 @@ public final class DefaultLoadControl implements LoadControl { @Override public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; + return minBufferDurationUs <= 0 + || bufferedDurationUs >= minBufferDurationUs + || (!prioritizeTimeOverSizeThresholds + && allocator.getTotalBytesAllocated() >= targetBufferSize); } @Override public boolean shouldContinueLoading(long bufferedDurationUs) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs // between watermarks - && isBuffering && !targetBufferSizeReached); + if (prioritizeTimeOverSizeThresholds) { + isBuffering = + bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering + && !targetBufferSizeReached); + } else { + isBuffering = + !targetBufferSizeReached + && (bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks + } if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -182,6 +238,25 @@ public final class DefaultLoadControl implements LoadControl { return isBuffering; } + /** + * Calculate target buffer size in bytes based on the selected tracks. The player will try not to + * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}. + * + * @param renderers The renderers for which the track were selected. + * @param trackSelectionArray The selected tracks. + * @return The target buffer size in bytes. + */ + protected int calculateTargetBufferSize( + Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + int targetBufferSize = 0; + for (int i = 0; i < renderers.length; i++) { + if (trackSelectionArray.get(i) != null) { + targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + } + } + return targetBufferSize; + } + private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 3fae0b8e6e756c1210636071088dd1fe7e9c080e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 11:59:36 -0800 Subject: [PATCH 141/148] Fix analyze/lint errors - Lint doesn't like a static import of something not available on the minimum API level. - The method linked to in the Javadoc was incorrect (wrong signature). I couldn't really work out why it was there, so I got rid of it rather than updating. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179222587 --- .../android/exoplayer2/audio/ChannelMappingAudioProcessor.java | 2 -- .../java/com/google/android/exoplayer2/video/DummySurface.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index c3f3e32526..17b90680dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -51,8 +51,6 @@ import java.util.Arrays; /** * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. - * - * @see AudioSink#configure(String, int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; 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 2c172c086b..9fcf89d628 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 @@ -24,7 +24,6 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; -import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -326,7 +325,7 @@ public final class DummySurface extends Surface { EGLSurface surface; if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { - surface = EGL_NO_SURFACE; + surface = EGL14.EGL_NO_SURFACE; } else { int[] pbufferAttributes; if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { From 16ad280817f68376d0017fdeb7cb16508571701a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 12:38:05 -0800 Subject: [PATCH 142/148] Bump version to 2.6.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179227114 --- constants.gradle | 2 +- demos/ima/src/main/AndroidManifest.xml | 4 ++-- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/constants.gradle b/constants.gradle index bad69389a5..c18fb28d4d 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = '2.6.0' + releaseVersion = '2.6.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index f14feeda74..0efeaf6f7f 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index ec8016e8a3..00326157a2 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index f13a7de0ca..b2200b6671 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.6.0"; + public static final String VERSION = "2.6.1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2006000; + public static final int VERSION_INT = 2006001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From ad95a147d24d2a16412d70ba5981ea1f8f5fa303 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 22 Dec 2017 05:33:11 -0800 Subject: [PATCH 143/148] Fix a bug that makes ClippingMediaSource not stop in some occasions. If ClippingMediaSource contains a child MediaSource with embedded metadata stream, and the embedded stream is being used, it can lead to ClippingMediaSource not be able to stop after the clipping end point. The reason being the metadata stream cannot read anymore sample, but it's also not end of source at that point. This CL fix this by changing the condition to check if the child stream cannot read anymore sample and it has read past the clipping end point. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179918038 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 89af07a3f0..d27c329845 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 @@ -278,9 +278,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb 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))) { + if (endUs != C.TIME_END_OF_SOURCE + && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) + || (result == C.RESULT_NOTHING_READ + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; From f657893973a27ee57dd1ece40ed19c30b9b33662 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:07:15 -0800 Subject: [PATCH 144/148] Make SsaDecoder more robust against malformed content Issue: #3645 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180559196 --- .../android/exoplayer2/text/ssa/SsaDecoder.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index eec4a1269c..0cb6f66898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -150,6 +150,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { break; } } + if (formatStartIndex == C.INDEX_UNSET + || formatEndIndex == C.INDEX_UNSET + || formatTextIndex == C.INDEX_UNSET) { + // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. + formatKeyCount = 0; + } } /** @@ -161,12 +167,17 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine); + Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); return; } String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) .split(",", formatKeyCount); + if (lineValues.length != formatKeyCount) { + Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); + return; + } + long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); From f8c76f62e40fb29913635fc6b0d8225698cd4bf3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Jan 2018 13:50:24 +0000 Subject: [PATCH 145/148] Fix ClippingSampleStream --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 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 d27c329845..1114a563b6 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 @@ -112,7 +112,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb if (internalStreams[i] == null) { sampleStreams[i] = null; } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, + sampleStreams[i] = new ClippingSampleStream(internalStreams[i], startUs, endUs, pendingInitialDiscontinuity); } streams[i] = sampleStreams[i]; @@ -222,9 +222,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** * Wraps a {@link SampleStream} and clips its samples. */ - private static final class ClippingSampleStream implements SampleStream { + private final class ClippingSampleStream implements SampleStream { - private final MediaPeriod mediaPeriod; private final SampleStream stream; private final long startUs; private final long endUs; @@ -232,9 +231,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb private boolean pendingDiscontinuity; private boolean sentEos; - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; + public ClippingSampleStream(SampleStream stream, long startUs, long endUs, + boolean pendingDiscontinuity) { this.stream = stream; this.startUs = startUs; this.endUs = endUs; From 42a3e2e9d2c4383c00bca1b2b032fedeb47aadb7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Dec 2017 07:35:30 -0800 Subject: [PATCH 146/148] Add support for extracting 32-bit float WAVE Issue: #3379 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179925320 --- RELEASENOTES.md | 2 ++ .../extractor/wav/WavHeaderReader.java | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a2347686d..ef0facd6e2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,8 @@ * Audio: * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). * Fix handling of playback parameter changes while paused when followed by a diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index 0e99380a1c..d0810a0629 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -31,6 +31,8 @@ import java.io.IOException; /** Integer PCM audio data. */ private static final int TYPE_PCM = 0x0001; + /** Float PCM audio data. */ + private static final int TYPE_FLOAT = 0x0003; /** Extended WAVE format. */ private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; @@ -87,14 +89,22 @@ import java.io.IOException; + blockAlignment); } - @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); - return null; + @C.PcmEncoding int encoding; + switch (type) { + case TYPE_PCM: + case TYPE_WAVE_FORMAT_EXTENSIBLE: + encoding = Util.getPcmEncoding(bitsPerSample); + break; + case TYPE_FLOAT: + encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; + break; + default: + Log.e(TAG, "Unsupported WAV format type: " + type); + return null; } - if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { - Log.e(TAG, "Unsupported WAV format type: " + type); + if (encoding == C.ENCODING_INVALID) { + Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type); return null; } From 134c494b1b4b6ae03706c3a6859ce264020856ae Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 27 Dec 2017 09:02:40 -0800 Subject: [PATCH 147/148] Typo fixes Issue:#3631 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180197723 --- README.md | 2 +- RELEASENOTES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecfe3eb96f..7f35329516 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ 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. +extensions are available from JCenter, whereas others must be built manually. Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ef0facd6e2..5fe0090727 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to to connect ExoPlayer with +* MediaSession extension: Provides an easy to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From b704a1d6436ba64a0d6fab2868d36a6b47030fdc Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 07:00:04 -0800 Subject: [PATCH 148/148] Typo fix ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180543378 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5fe0090727..9d949570d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to connect ExoPlayer with +* MediaSession extension: Provides an easy way to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout