diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index ea8ade2f61..54fdeb283a 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -2,6 +2,18 @@
### dev-v2 (not yet released)
+* Core library:
+ * Log a warning when `SingleSampleMediaPeriod` transforms a load error
+ into end-of-stream.
+* Extractors:
+ * Fix Vorbis private codec data parsing in the Matroska extractor
+ ([#8496](https://github.com/google/ExoPlayer/issues/8496)).
+* Text:
+ * Add support for the SSA `primaryColour` style attribute
+ ([#8435](https://github.com/google/ExoPlayer/issues/8435)).
+
+### 2.13.0 (not yet released - targeted for 2021-02-TBD)
+
* Core library:
* Remove long deprecated symbols:
* `AdaptiveMediaSourceEventListener`. Use `MediaSourceEventListener`
@@ -80,8 +92,8 @@
`MediaSourceEventListener` and `SingleSampleMediaSource.Factory`
* `SimpleExoPlayer.addVideoDebugListener`,
`SimpleExoPlayer.removeVideoDebugListener`,
- `SimpleExoPlayer.addAudioDebugListener`
- and `SimpleExoPlayer.removeAudioDebugListener`. Use
+ `SimpleExoPlayer.addAudioDebugListener` and
+ `SimpleExoPlayer.removeAudioDebugListener`. Use
`SimpleExoPlayer.addAnalyticsListener` and
`SimpleExoPlayer.removeAnalyticsListener` instead.
* `AdaptiveMediaSourceEventListener`. Use `MediaSourceEventListener`
@@ -126,16 +138,18 @@
* Add option to `MergingMediaSource` to clip the durations of all sources
to have the same length
([#8422](https://github.com/google/ExoPlayer/issues/8422)).
- * Fix propagation of `LoadErrorHandlingPolicy` from
- `DefaultMediaSourceFactory` into `SingleSampleMediaSource.Factory` when
- creating subtitle media sources from
- `MediaItem.playbackProperties.subtitles`
- ([#8430](https://github.com/google/ExoPlayer/issues/8430)).
* Remove `ExoPlaybackException.OutOfMemoryError`.
* Remove `setVideoDecoderOutputBufferRenderer` from Player API. Clients
should use `setOutputSurface` directly instead.
* Default `SingleSampleMediaSource.treatLoadErrorsAsEndOfStream` to `true`
([#8430](https://github.com/google/ExoPlayer/issues/8430)).
+ * Remove `setVideoDecoderOutputBufferRenderer` from Player API. Use
+ `setVideoSurfaceView` and `clearVideoSurfaceView` instead.
+ * Replace `PlayerMessage.setHandler` with `PlayerMessage.setLooper`.
+* Transformer:
+ * Add a library to transform media inputs. Available transformations are:
+ configuration of output container format, removal of audio or video
+ track and slow motion flattening.
* Extractors:
* Populate codecs string for H.264/AVC in MP4, Matroska and FLV streams to
allow decoder capability checks based on codec profile/level
@@ -145,7 +159,10 @@
([#8393](https://github.com/google/ExoPlayer/issues/8393)).
* Handle sample size mismatches between raw audio `stsd` information and
`stsz` fixed sample size in MP4 extractors.
+ * Add support for playing JPEG motion photos
+ ([#5405](https://github.com/google/ExoPlayer/issues/5405)).
* Track selection:
+ * Moved `Player.getTrackSelector` to the `ExoPlayer` interface.
* Allow parallel adaptation for video and audio
([#5111](https://github.com/google/ExoPlayer/issues/5111)).
* Simplified enabling tunneling with `DefaultTrackSelector`.
@@ -157,6 +174,10 @@
([#8320](https://github.com/google/ExoPlayer/issues/8320)).
* Add option to specify preferred audio role flags.
* Forward `Timeline` and `MediaPeriodId` to `TrackSelection.Factory`.
+ * In order to make it immutable, `TrackSelection` in the `Player` API now
+ only contains methods related to static selection.
+ The rest of the methods have been moved to the child
+ class `ExoTrackSelection` which is used by all ExoPlayer components.
* DASH:
* Support low-latency DASH playback (`availabilityTimeOffset` and
`ServiceDescription` tags)
@@ -181,12 +202,24 @@
Widevine or Clearkey protected content in a playlist.
* Add `ExoMediaDrm.KeyRequest.getRequestType`
([#7847](https://github.com/google/ExoPlayer/issues/7847)).
+ * Drop key & provision responses if `DefaultDrmSession` is released while
+ waiting for the response. This fixes (harmless) `IllegalStateException:
+ sending message to a Handler on a dead thread` log messages
+ ([#8328](https://github.com/google/ExoPlayer/issues/8328)).
+ * Allow apps to fully customize DRM behaviour per-`MediaItem` by passing a
+ `DrmSessionManagerProvider` to `MediaSourceFactory`
+ ([#8466](https://github.com/google/ExoPlayer/issues/8466)).
* Analytics:
* Pass a `DecoderReuseEvaluation` to `AnalyticsListener`'s
`onVideoInputFormatChanged` and `onAudioInputFormatChanged` methods. The
`DecoderReuseEvaluation` indicates whether it was possible to re-use an
existing decoder instance for the new format, and if not then the
reasons why.
+* Video:
+ * Fix VP9 format capability checks on API level 23 and earlier. The
+ platform does not correctly report the VP9 level supported by the
+ decoder in this case, so we estimate it based on the decoder's maximum
+ supported bitrate.
* Audio:
* Fix handling of audio session IDs
([#8190](https://github.com/google/ExoPlayer/issues/8190)).
@@ -199,10 +232,13 @@
`onAudioSessionIdChanged` is called in fewer cases than
`onAudioSessionId` was called, due to the improved handling of audio
session IDs as described above.
+ * Retry playback after some types of `AudioTrack` error.
+ * Create E-AC3 JOC passthrough `AudioTrack`s using the maximum supported
+ channel count (instead of assuming 6 channels) from API 29.
* Text:
- * Gracefully handle null-terminated subtitle content in Matroska
- containers.
- * Fix CEA-708 anchor positioning
+ * Fix CEA-708 sequence number discontinuity handling
+ ([#1807](https://github.com/google/ExoPlayer/issues/1807)).
+ * Fix CEA-708 handling of unexpectedly small packets
([#1807](https://github.com/google/ExoPlayer/issues/1807)).
* Data sources:
* Use the user agent of the underlying network stack by default.
@@ -217,9 +253,17 @@
ad view group
([#7344](https://github.com/google/ExoPlayer/issues/7344)),
([#8339](https://github.com/google/ExoPlayer/issues/8339)).
- * Fix a bug that could cause the next content position played after a
- seek to snap back to the cue point of the preceding ad, rather than
- the requested content position.
+ * Fix a bug that could cause the next content position played after a seek
+ to snap back to the cue point of the preceding ad, rather than the
+ requested content position.
+ * Fix a regression that caused an ad group to be skipped after an initial
+ seek to a non-zero position. Unsupported VPAID ads will still be
+ skipped but only after the preload timeout rather than instantly
+ ([#8428](https://github.com/google/ExoPlayer/issues/8428)),
+ ([#7832](https://github.com/google/ExoPlayer/issues/7832)).
+ * Fix a regression that caused a short ad followed by another ad to be
+ skipped due to playback being stuck buffering waiting for the second ad
+ to load ([#8492](https://github.com/google/ExoPlayer/issues/8492)).
* FFmpeg extension:
* Link the FFmpeg library statically, saving 350KB in binary size on
average.
@@ -300,7 +344,6 @@
* Support enabling the previous and next actions individually in
`PlayerNotificationManager`.
* Audio:
- * Retry playback after some types of `AudioTrack` error.
* Work around `AudioManager` crashes when calling `getStreamVolume`
([#8191](https://github.com/google/ExoPlayer/issues/8191)).
* Extractors:
diff --git a/constants.gradle b/constants.gradle
index ba74fccbcf..bb775e7050 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
- releaseVersion = '2.12.3'
- releaseVersionCode = 2012003
+ releaseVersion = '2.13.0'
+ releaseVersionCode = 2013000
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
@@ -24,7 +24,7 @@ project.ext {
guavaVersion = '27.1-android'
mockitoVersion = '2.28.2'
mockWebServerVersion = '3.12.0'
- robolectricVersion = '4.5-alpha-3'
+ robolectricVersion = '4.5'
checkerframeworkVersion = '3.3.0'
checkerframeworkCompatVersion = '2.5.0'
jsr305Version = '3.0.2'
diff --git a/core_settings.gradle b/core_settings.gradle
index bd217a37e5..241b94a19b 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -28,6 +28,7 @@ include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
+include modulePrefix + 'library-transformer'
include modulePrefix + 'library-ui'
include modulePrefix + 'robolectricutils'
include modulePrefix + 'testutils'
@@ -56,6 +57,7 @@ project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/d
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
+project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java
index dc0a8b990a..191602dfb8 100644
--- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java
+++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java
@@ -152,7 +152,7 @@ public final class MainActivity extends Activity {
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
- drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
}
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
index eb669ecf94..a31cd7efe0 100644
--- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
+++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java
@@ -197,7 +197,7 @@ public final class MainActivity extends Activity {
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
- drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
+ drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
}
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle
index 4c8f648e34..d0cc501fcb 100644
--- a/extensions/cast/build.gradle
+++ b/extensions/cast/build.gradle
@@ -16,7 +16,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
api 'com.google.android.gms:play-services-cast-framework:18.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
index 7bc9fadf42..d20b84cbc3 100644
--- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java
@@ -30,11 +30,10 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.metadata.Metadata;
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.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -138,6 +137,7 @@ public final class CastPlayer extends BasePlayer {
listeners =
new ListenerSet<>(
Looper.getMainLooper(),
+ Clock.DEFAULT,
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
@@ -504,12 +504,6 @@ public final class CastPlayer extends BasePlayer {
}
}
- @Override
- @Nullable
- public TrackSelector getTrackSelector() {
- return null;
- }
-
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) {
@@ -771,7 +765,7 @@ public final class CastPlayer extends BasePlayer {
int rendererIndex = getRendererIndexForTrackType(trackType);
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
&& trackSelections[rendererIndex] == null) {
- trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
+ trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]);
}
}
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTrackSelection.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTrackSelection.java
new file mode 100644
index 0000000000..22fe86d9e4
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTrackSelection.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.cast;
+
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * {@link TrackSelection} that only selects the first track of the provided {@link TrackGroup}.
+ *
+ *
This relies on {@link CastPlayer} track groups only having one track.
+ */
+/* package */ class CastTrackSelection implements TrackSelection {
+
+ private final TrackGroup trackGroup;
+
+ /** @param trackGroup The {@link TrackGroup} from which the first track will only be selected. */
+ public CastTrackSelection(TrackGroup trackGroup) {
+ this.trackGroup = trackGroup;
+ }
+
+ @Override
+ public TrackGroup getTrackGroup() {
+ return trackGroup;
+ }
+
+ @Override
+ public int length() {
+ return 1;
+ }
+
+ @Override
+ public Format getFormat(int index) {
+ Assertions.checkArgument(index == 0);
+ return trackGroup.getFormat(0);
+ }
+
+ @Override
+ public int getIndexInTrackGroup(int index) {
+ return index == 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public int indexOf(Format format) {
+ return format == trackGroup.getFormat(0) ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int indexOf(int indexInTrackGroup) {
+ return indexInTrackGroup == 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ // Object overrides.
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(trackGroup);
+ }
+
+ // Track groups are compared by identity not value, as distinct groups may have the same value.
+ @Override
+ @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ CastTrackSelection other = (CastTrackSelection) obj;
+ return trackGroup == other.trackGroup;
+ }
+}
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTrackSelectionTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTrackSelectionTest.java
new file mode 100644
index 0000000000..0a30c0c4b8
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTrackSelectionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.cast;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test for {@link CastTrackSelection}. */
+@RunWith(AndroidJUnit4.class)
+public class CastTrackSelectionTest {
+
+ private static final TrackGroup TRACK_GROUP =
+ new TrackGroup(new Format.Builder().build(), new Format.Builder().build());
+
+ private static final CastTrackSelection SELECTION = new CastTrackSelection(TRACK_GROUP);
+
+ @Test
+ public void length_isOne() {
+ assertThat(SELECTION.length()).isEqualTo(1);
+ }
+
+ @Test
+ public void getTrackGroup_returnsSameGroup() {
+ assertThat(SELECTION.getTrackGroup()).isSameInstanceAs(TRACK_GROUP);
+ }
+
+ @Test
+ public void getFormatSelectedTrack_isFirstTrack() {
+ assertThat(SELECTION.getFormat(0)).isSameInstanceAs(TRACK_GROUP.getFormat(0));
+ }
+
+ @Test
+ public void getIndexInTrackGroup_ofSelectedTrack_returnsFirstTrack() {
+ assertThat(SELECTION.getIndexInTrackGroup(0)).isEqualTo(0);
+ }
+
+ @Test
+ public void getIndexInTrackGroup_onePastTheEnd_returnsIndexUnset() {
+ assertThat(SELECTION.getIndexInTrackGroup(1)).isEqualTo(C.INDEX_UNSET);
+ }
+
+ @Test
+ public void indexOf_selectedTrack_returnsFirstTrack() {
+ assertThat(SELECTION.indexOf(0)).isEqualTo(0);
+ }
+
+ @Test
+ public void indexOf_onePastTheEnd_returnsIndexUnset() {
+ assertThat(SELECTION.indexOf(1)).isEqualTo(C.INDEX_UNSET);
+ }
+
+ @Test(expected = Exception.class)
+ public void getFormat_outOfBound_throws() {
+ CastTrackSelection selection = new CastTrackSelection(TRACK_GROUP);
+
+ selection.getFormat(1);
+ }
+}
diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle
index b99512935e..19526461ff 100644
--- a/extensions/cronet/build.gradle
+++ b/extensions/cronet/build.gradle
@@ -13,12 +13,25 @@
// limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
+android {
+ defaultConfig {
+ multiDexEnabled true
+ }
+}
+
dependencies {
api "com.google.android.gms:play-services-cronet:17.0.0"
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
+ androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
+ androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
+ androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
+ // Emulator tests assume that an app-packaged version of cronet is
+ // available.
+ androidTestImplementation 'org.chromium.net:cronet-embedded:76.3809.111'
+ androidTestImplementation(project(modulePrefix + 'testutils'))
testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..96e8e54f57
--- /dev/null
+++ b/extensions/cronet/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceContractTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceContractTest.java
new file mode 100644
index 0000000000..967c894c39
--- /dev/null
+++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceContractTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.cronet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.common.collect.ImmutableList;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link CronetDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class CronetDataSourceContractTest extends DataSourceContractTest {
+
+ @Rule public HttpDataSourceTestEnv httpDataSourceTestEnv = new HttpDataSourceTestEnv();
+ private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+ @After
+ public void tearDown() {
+ executorService.shutdown();
+ }
+
+ @Override
+ protected DataSource createDataSource() {
+ CronetEngineWrapper cronetEngineWrapper =
+ new CronetEngineWrapper(
+ ApplicationProvider.getApplicationContext(),
+ /* userAgent= */ "test-agent",
+ /* preferGMSCoreCronet= */ false);
+ assertThat(cronetEngineWrapper.getCronetEngineSource())
+ .isEqualTo(CronetEngineWrapper.SOURCE_NATIVE);
+ return new CronetDataSource.Factory(cronetEngineWrapper, executorService)
+ .setFallbackFactory(new InvalidDataSourceFactory())
+ .createDataSource();
+ }
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return httpDataSourceTestEnv.getServedResources();
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl());
+ }
+
+ /**
+ * An {@link HttpDataSource.Factory} that throws {@link UnsupportedOperationException} on every
+ * interaction.
+ */
+ private static class InvalidDataSourceFactory implements HttpDataSource.Factory {
+ @Override
+ public HttpDataSource createDataSource() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public HttpDataSource.RequestProperties getDefaultRequestProperties() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public HttpDataSource.Factory setDefaultRequestProperties(
+ Map defaultRequestProperties) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index 9798eea656..2726b00c73 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.cronet;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
-import static java.lang.Math.min;
import android.net.Uri;
import android.text.TextUtils;
@@ -37,6 +36,7 @@ import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate;
+import com.google.common.primitives.Ints;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
@@ -655,14 +655,21 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
readBuffer.flip();
Assertions.checkState(readBuffer.hasRemaining());
if (bytesToSkip > 0) {
- int bytesSkipped = (int) min(readBuffer.remaining(), bytesToSkip);
+ int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
readBuffer.position(readBuffer.position() + bytesSkipped);
bytesToSkip -= bytesSkipped;
}
}
}
- int bytesRead = min(readBuffer.remaining(), readLength);
+ // Ensure we read up to bytesRemaining, in case this was a Range request with finite end, but
+ // the server does not support Range requests and transmitted the entire resource.
+ int bytesRead =
+ Ints.min(
+ bytesRemaining != C.LENGTH_UNSET ? (int) bytesRemaining : Integer.MAX_VALUE,
+ readBuffer.remaining(),
+ readLength);
+
readBuffer.get(buffer, offset, bytesRead);
if (bytesRemaining != C.LENGTH_UNSET) {
@@ -1039,7 +1046,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// Copy as much as possible from the src buffer into dst buffer.
// Returns the number of bytes copied.
private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
- int remaining = min(src.remaining(), dst.remaining());
+ int remaining = Math.min(src.remaining(), dst.remaining());
int limit = src.limit();
src.limit(src.position() + remaining);
dst.put(src);
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
index f979e99b7d..df3e9549e5 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java
@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.cronet;
import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.TransferListener;
@@ -25,8 +25,6 @@ import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
/** @deprecated Use {@link CronetDataSource.Factory} instead. */
-// Uses deprecated DefaultHttpDataSourceFactory
-@SuppressWarnings("deprecation")
@Deprecated
public final class CronetDataSourceFactory extends BaseFactory {
@@ -82,7 +80,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
* Creates an instance.
*
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -98,7 +96,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -118,19 +116,14 @@ public final class CronetDataSourceFactory extends BaseFactory {
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
false,
- new DefaultHttpDataSourceFactory(
- userAgent,
- /* listener= */ null,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
- false));
+ new DefaultHttpDataSource.Factory().setUserAgent(userAgent));
}
/**
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
@@ -152,15 +145,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
cronetEngineWrapper,
executor,
/* transferListener= */ null,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
+ connectTimeoutMs,
+ readTimeoutMs,
resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(
- userAgent,
- /* listener= */ null,
- connectTimeoutMs,
- readTimeoutMs,
- resetTimeoutOnRedirects));
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setConnectTimeoutMs(connectTimeoutMs)
+ .setReadTimeoutMs(readTimeoutMs));
}
/**
@@ -228,7 +219,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -248,7 +239,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
*
Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
@@ -272,19 +263,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS,
false,
- new DefaultHttpDataSourceFactory(
- userAgent,
- transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
- false));
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setTransferListener(transferListener));
}
/**
* Creates an instance.
*
*
If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
- * DefaultHttpDataSourceFactory} will be used instead.
+ * DefaultHttpDataSource.Factory} will be used instead.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
@@ -308,11 +296,14 @@ public final class CronetDataSourceFactory extends BaseFactory {
cronetEngineWrapper,
executor,
transferListener,
- DEFAULT_CONNECT_TIMEOUT_MILLIS,
- DEFAULT_READ_TIMEOUT_MILLIS,
+ connectTimeoutMs,
+ readTimeoutMs,
resetTimeoutOnRedirects,
- new DefaultHttpDataSourceFactory(
- userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
+ new DefaultHttpDataSource.Factory()
+ .setUserAgent(userAgent)
+ .setTransferListener(transferListener)
+ .setConnectTimeoutMs(connectTimeoutMs)
+ .setReadTimeoutMs(readTimeoutMs));
}
/**
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java
index 551c88ac3d..4ce0610fb6 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTagLoader.java
@@ -451,25 +451,10 @@ import java.util.Map;
return;
}
- if (playbackState == Player.STATE_BUFFERING && !player.isPlayingAd()) {
- // Check whether we are waiting for an ad to preload.
- int adGroupIndex = getLoadingAdGroupIndex();
- if (adGroupIndex == C.INDEX_UNSET) {
- return;
- }
- AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
- if (adGroup.count != C.LENGTH_UNSET
- && adGroup.count != 0
- && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
- // An ad is available already so we must be buffering for some other reason.
- return;
- }
- long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
- long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
- long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
- if (timeUntilAdMs < configuration.adPreloadTimeoutMs) {
- waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
- }
+ if (playbackState == Player.STATE_BUFFERING
+ && !player.isPlayingAd()
+ && isWaitingForAdToLoad()) {
+ waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
} else if (playbackState == Player.STATE_READY) {
waitingForPreloadElapsedRealtimeMs = C.TIME_UNSET;
}
@@ -759,27 +744,35 @@ import java.util.Map;
if (imaAdInfo != null) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
updateAdPlaybackState();
- } else {
- // Mark any ads for the current/reported player position that haven't loaded as being in the
- // error state, to force resuming content. This includes VPAID ads that never load.
- long playerPositionUs;
- if (player != null) {
- playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period));
- } else if (!VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(lastContentProgress)) {
- // Playback is backgrounded so use the last reported content position.
- playerPositionUs = C.msToUs(lastContentProgress.getCurrentTimeMs());
- } else {
- return;
- }
- int adGroupIndex =
- adPlaybackState.getAdGroupIndexForPositionUs(
- playerPositionUs, C.msToUs(contentDurationMs));
- if (adGroupIndex != C.INDEX_UNSET) {
- markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
- }
}
}
+ /**
+ * Returns whether this instance is expecting the first ad in an the upcoming ad group to load
+ * within the {@link ImaUtil.Configuration#adPreloadTimeoutMs preload timeout}.
+ */
+ private boolean isWaitingForAdToLoad() {
+ @Nullable Player player = this.player;
+ if (player == null) {
+ return false;
+ }
+ int adGroupIndex = getLoadingAdGroupIndex();
+ if (adGroupIndex == C.INDEX_UNSET) {
+ return false;
+ }
+ AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
+ if (adGroup.count != C.LENGTH_UNSET
+ && adGroup.count != 0
+ && adGroup.states[0] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
+ // An ad is available already.
+ return false;
+ }
+ long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
+ long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
+ long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
+ return timeUntilAdMs < configuration.adPreloadTimeoutMs;
+ }
+
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
@@ -1305,6 +1298,12 @@ import java.util.Map;
handleAdGroupLoadError(new IOException("Ad preloading timed out"));
maybeNotifyPendingAdLoadError();
}
+ } else if (pendingContentPositionMs != C.TIME_UNSET
+ && player != null
+ && player.getPlaybackState() == Player.STATE_BUFFERING
+ && isWaitingForAdToLoad()) {
+ // Prepare to timeout the load of an ad for the pending seek operation.
+ waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
}
return videoProgressUpdate;
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 5eae985fcd..e2adbaf2d0 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
@@ -60,7 +60,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
/**
@@ -700,7 +699,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
@Override
public ImaSdkSettings createImaSdkSettings() {
ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings();
- settings.setLanguage(getImaLanguageCodeForDefaultLocale());
+ settings.setLanguage(Util.getSystemLanguageCodes()[0]);
return settings;
}
@@ -742,17 +741,5 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
return ImaSdkFactory.getInstance()
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
}
-
- /**
- * Returns a language code that's suitable for passing to {@link ImaSdkSettings#setLanguage} and
- * corresponds to the device's {@link Locale#getDefault() default Locale}. IMA will fall back to
- * its default language code ("en") if the value returned is unsupported.
- */
- // TODO: It may be possible to define a better mapping onto IMA's supported language codes. See:
- // https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization.
- // [Internal ref: b/174042000] will help if implemented.
- private static String getImaLanguageCodeForDefaultLocale() {
- return Util.splitAtFirst(Util.getSystemLanguageCodes()[0], "-")[0];
- }
}
}
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
index a5802fad0d..6b62af93f3 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java
@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
/** A fake player for testing content/ad playback. */
@@ -43,6 +44,7 @@ import com.google.android.exoplayer2.util.ListenerSet;
listeners =
new ListenerSet<>(
Looper.getMainLooper(),
+ Clock.DEFAULT,
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
period = new Timeline.Period();
diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
index f7f5ef43fd..e7b6603694 100644
--- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
+++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java
@@ -450,6 +450,62 @@ public final class ImaAdsLoaderTest {
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
+ @Test
+ public void startPlaybackAfterMidroll_doesNotSkipMidroll() {
+ // Simulate an ad at 2 seconds, and starting playback with an initial seek position at the ad.
+ long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
+ long adGroupTimeUs =
+ adGroupPositionInWindowUs
+ + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
+ ImmutableList cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
+ when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
+ fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
+ fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(adGroupPositionInWindowUs));
+
+ // Start ad loading while still buffering and simulate the calls from the IMA SDK to resume then
+ // immediately pause content playback.
+ imaAdsLoader.start(
+ adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
+ contentProgressProvider.getContentProgress();
+ adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
+ contentProgressProvider.getContentProgress();
+ adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, /* ad= */ null));
+ contentProgressProvider.getContentProgress();
+
+ assertThat(getAdPlaybackState(/* periodIndex= */ 0))
+ .isEqualTo(
+ new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints))
+ .withContentDurationUs(CONTENT_PERIOD_DURATION_US));
+ }
+
+ @Test
+ public void startPlaybackAfterMidroll_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
+ // Simulate an ad at 2 seconds, and starting playback with an initial seek position at the ad.
+ long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
+ long adGroupTimeUs =
+ adGroupPositionInWindowUs
+ + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
+ ImmutableList cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
+ when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
+ fakePlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
+ fakePlayer.setPlayingContentPosition(/* periodIndex= */ 0, C.usToMs(adGroupPositionInWindowUs));
+
+ // Start ad loading while still buffering and poll progress without the ad loading.
+ imaAdsLoader.start(
+ adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
+ contentProgressProvider.getContentProgress();
+ ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
+ contentProgressProvider.getContentProgress();
+
+ assertThat(getAdPlaybackState(/* periodIndex= */ 0))
+ .isEqualTo(
+ new AdPlaybackState(TEST_ADS_ID, getAdGroupTimesUsForCuePoints(cuePoints))
+ .withContentDurationUs(CONTENT_PERIOD_DURATION_US)
+ .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
+ }
+
@Test
public void bufferingDuringAd_callsOnBuffering() {
// Load the preroll ad.
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
index 032fb0fded..758eb646f6 100644
--- a/extensions/okhttp/build.gradle
+++ b/extensions/okhttp/build.gradle
@@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
diff --git a/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceContractTest.java b/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceContractTest.java
new file mode 100644
index 0000000000..8bf572d70d
--- /dev/null
+++ b/extensions/okhttp/src/test/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceContractTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.okhttp;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.common.collect.ImmutableList;
+import okhttp3.OkHttpClient;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link OkHttpDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class OkHttpDataSourceContractTest extends DataSourceContractTest {
+
+ @Rule public HttpDataSourceTestEnv httpDataSourceTestEnv = new HttpDataSourceTestEnv();
+
+ @Override
+ protected DataSource createDataSource() {
+ return new OkHttpDataSource.Factory(new OkHttpClient()).createDataSource();
+ }
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return httpDataSourceTestEnv.getServedResources();
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl());
+ }
+}
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index 3d912bebf6..7a37396568 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -14,10 +14,11 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
- implementation project(modulePrefix + 'library-core')
+ implementation project(modulePrefix + 'library-common')
implementation 'net.butterflytv.utils:rtmp-client:3.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
+ testImplementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
diff --git a/library/common/build.gradle b/library/common/build.gradle
index de0df42506..d1d0d86f42 100644
--- a/library/common/build.gradle
+++ b/library/common/build.gradle
@@ -35,7 +35,9 @@ dependencies {
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion
+ testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+ testImplementation project(modulePrefix + 'testutils')
}
ext {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java
rename to library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java
index 7be52b43e0..1c2cc92362 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java
@@ -1080,23 +1080,6 @@ public final class C {
/** Indicates the track is intended for trick play. */
public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14;
- // TODO(b/172315872) Move usage back to Player.RepeatMode when Player is moved in common.
- /**
- * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
- * #REPEAT_MODE_ALL}.
- */
- @Documented
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
- static @interface RepeatMode {}
-
- /** Normal playback without repetition. */
- /* package */ static final int REPEAT_MODE_OFF = 0;
- /** "Repeat One" mode to repeat the currently playing window infinitely. */
- /* package */ static final int REPEAT_MODE_ONE = 1;
- /** "Repeat All" mode to repeat the entire timeline infinitely. */
- /* package */ static final int REPEAT_MODE_ALL = 2;
-
/**
* Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link
* #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index 94cbe24033..21f352590c 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.12.3";
+ public static final String VERSION = "2.13.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.12.3";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.0";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -44,7 +44,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 = 2012003;
+ public static final int VERSION_INT = 2013000;
/**
* The default user agent for requests made by the library.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java
similarity index 98%
rename from library/core/src/main/java/com/google/android/exoplayer2/Player.java
rename to library/common/src/main/java/com/google/android/exoplayer2/Player.java
index 667d26e761..2a92964649 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java
@@ -34,7 +34,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectorInterface;
import com.google.android.exoplayer2.util.MutableFlags;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
@@ -618,11 +617,14 @@ public interface Player {
default void onSeekProcessed() {}
/**
- * Called when the player has started or stopped offload scheduling after a call to {@link
+ * Called when the player has started or stopped offload scheduling.
+ *
+ * If using ExoPlayer, this is done by calling {@code
* ExoPlayer#experimentalSetOffloadSchedulingEnabled(boolean)}.
*
*
This method is experimental, and will be renamed or removed in a future release.
*/
+ // TODO(b/172315872) Move this method in a new ExoPlayer.EventListener.
default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {}
/**
@@ -740,9 +742,7 @@ public interface Player {
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})
@interface State {}
- /**
- * The player does not have any media to play.
- */
+ /** The player does not have any media to play. */
int STATE_IDLE = 1;
/**
* The player is not able to immediately play from its current position. This state typically
@@ -754,9 +754,7 @@ public interface Player {
* {@link #getPlayWhenReady()} is true, and paused otherwise.
*/
int STATE_READY = 3;
- /**
- * The player has finished playing the media.
- */
+ /** The player has finished playing the media. */
int STATE_ENDED = 4;
/**
@@ -817,20 +815,20 @@ public interface Player {
* Normal playback without repetition. "Previous" and "Next" actions move to the previous and next
* windows respectively, and do nothing when there is no previous or next window to move to.
*/
- int REPEAT_MODE_OFF = C.REPEAT_MODE_OFF;
+ int REPEAT_MODE_OFF = 0;
/**
* Repeats the currently playing window infinitely during ongoing playback. "Previous" and "Next"
* actions behave as they do in {@link #REPEAT_MODE_OFF}, moving to the previous and next windows
* respectively, and doing nothing when there is no previous or next window to move to.
*/
- int REPEAT_MODE_ONE = C.REPEAT_MODE_ONE;
+ int REPEAT_MODE_ONE = 1;
/**
* Repeats the entire timeline infinitely. "Previous" and "Next" actions behave as they do in
* {@link #REPEAT_MODE_OFF}, but with looping at the ends so that "Previous" when playing the
* first window will move to the last window, and "Next" when playing the last window will move to
* the first window.
*/
- int REPEAT_MODE_ALL = C.REPEAT_MODE_ALL;
+ int REPEAT_MODE_ALL = 2;
/**
* Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_PERIOD_TRANSITION},
@@ -1218,7 +1216,8 @@ public interface Player {
*
* @return The current repeat mode.
*/
- @RepeatMode int getRepeatMode();
+ @RepeatMode
+ int getRepeatMode();
/**
* Sets whether shuffling of windows is enabled.
@@ -1227,9 +1226,7 @@ public interface Player {
*/
void setShuffleModeEnabled(boolean shuffleModeEnabled);
- /**
- * Returns whether shuffling of windows is enabled.
- */
+ /** Returns whether shuffling of windows is enabled. */
boolean getShuffleModeEnabled();
/**
@@ -1364,9 +1361,7 @@ public interface Player {
*/
void release();
- /**
- * Returns the number of renderers.
- */
+ /** Returns the number of renderers. */
int getRendererCount();
/**
@@ -1380,12 +1375,6 @@ public interface Player {
*/
int getRendererType(int index);
- /**
- * Returns the track selector that this player uses, or null if track selection is not supported.
- */
- @Nullable
- TrackSelectorInterface getTrackSelector();
-
/** Returns the available track groups. */
TrackGroupArray getCurrentTrackGroups();
@@ -1411,14 +1400,10 @@ public interface Player {
@Nullable
Object getCurrentManifest();
- /**
- * Returns the current {@link Timeline}. Never null, but may be empty.
- */
+ /** Returns the current {@link Timeline}. Never null, but may be empty. */
Timeline getCurrentTimeline();
- /**
- * Returns the index of the period currently being played.
- */
+ /** Returns the index of the period currently being played. */
int getCurrentPeriodIndex();
/**
@@ -1538,9 +1523,7 @@ public interface Player {
*/
boolean isCurrentWindowSeekable();
- /**
- * Returns whether the player is currently playing an ad.
- */
+ /** Returns whether the player is currently playing an ad. */
boolean isPlayingAd();
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
index 842862fee8..d7e1e955db 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -734,14 +734,14 @@ public abstract class Timeline {
* @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window.
*/
public int getNextWindowIndex(
- int windowIndex, @C.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
+ int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
switch (repeatMode) {
- case C.REPEAT_MODE_OFF:
+ case Player.REPEAT_MODE_OFF:
return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET
: windowIndex + 1;
- case C.REPEAT_MODE_ONE:
+ case Player.REPEAT_MODE_ONE:
return windowIndex;
- case C.REPEAT_MODE_ALL:
+ case Player.REPEAT_MODE_ALL:
return windowIndex == getLastWindowIndex(shuffleModeEnabled)
? getFirstWindowIndex(shuffleModeEnabled) : windowIndex + 1;
default:
@@ -759,14 +759,14 @@ public abstract class Timeline {
* @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window.
*/
public int getPreviousWindowIndex(
- int windowIndex, @C.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
+ int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
switch (repeatMode) {
- case C.REPEAT_MODE_OFF:
+ case Player.REPEAT_MODE_OFF:
return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET
: windowIndex - 1;
- case C.REPEAT_MODE_ONE:
+ case Player.REPEAT_MODE_ONE:
return windowIndex;
- case C.REPEAT_MODE_ALL:
+ case Player.REPEAT_MODE_ALL:
return windowIndex == getFirstWindowIndex(shuffleModeEnabled)
? getLastWindowIndex(shuffleModeEnabled) : windowIndex - 1;
default:
@@ -847,7 +847,7 @@ public abstract class Timeline {
int periodIndex,
Period period,
Window window,
- @C.RepeatMode int repeatMode,
+ @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
int windowIndex = getPeriod(periodIndex, period).windowIndex;
if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) {
@@ -875,7 +875,7 @@ public abstract class Timeline {
int periodIndex,
Period period,
Window window,
- @C.RepeatMode int repeatMode,
+ @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled)
== C.INDEX_UNSET;
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
index 71ffb00982..35800e6bbd 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.audio;
+import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
@@ -33,6 +34,11 @@ import com.google.android.exoplayer2.util.Util;
*/
public final class AudioAttributes {
+ private static final String FIELD_CONTENT_TYPE = "contentType";
+ private static final String FIELD_FLAGS = "flags";
+ private static final String FIELD_USAGE = "usage";
+ private static final String FIELD_ALLOWED_CAPTURE_POLICY = "allowedCapturePolicy";
+
public static final AudioAttributes DEFAULT = new Builder().build();
/**
@@ -159,4 +165,31 @@ public final class AudioAttributes {
return result;
}
+ /** Converts this instance into a {@link Bundle}. */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(FIELD_CONTENT_TYPE, contentType);
+ bundle.putInt(FIELD_FLAGS, flags);
+ bundle.putInt(FIELD_USAGE, usage);
+ bundle.putInt(FIELD_ALLOWED_CAPTURE_POLICY, allowedCapturePolicy);
+ return bundle;
+ }
+
+ /** Creates an {@link AudioAttributes} instance from a {@link Bundle}. */
+ public static AudioAttributes fromBundle(Bundle bundle) {
+ Builder builder = new Builder();
+ if (bundle.containsKey(FIELD_CONTENT_TYPE)) {
+ builder.setContentType(bundle.getInt(FIELD_CONTENT_TYPE));
+ }
+ if (bundle.containsKey(FIELD_FLAGS)) {
+ builder.setFlags(bundle.getInt(FIELD_FLAGS));
+ }
+ if (bundle.containsKey(FIELD_USAGE)) {
+ builder.setUsage(bundle.getInt(FIELD_USAGE));
+ }
+ if (bundle.containsKey(FIELD_ALLOWED_CAPTURE_POLICY)) {
+ builder.setAllowedCapturePolicy(bundle.getInt(FIELD_ALLOWED_CAPTURE_POLICY));
+ }
+ return builder.build();
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java
index 8d662c318e..b640d7a820 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceInfo.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.device;
+import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import java.lang.annotation.Documented;
@@ -26,6 +27,10 @@ import java.lang.annotation.Target;
/** Information about the playback device. */
public final class DeviceInfo {
+ private static final String FIELD_PLAYBACK_TYPE = "playbackType";
+ private static final String FIELD_MIN_VOLUME = "minVolume";
+ private static final String FIELD_MAX_VOLUME = "maxVolume";
+
/** Types of playback. One of {@link #PLAYBACK_TYPE_LOCAL} or {@link #PLAYBACK_TYPE_REMOTE}. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@@ -80,4 +85,21 @@ public final class DeviceInfo {
result = 31 * result + maxVolume;
return result;
}
+
+ /** Converts this instance into a {@link Bundle}. */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType);
+ bundle.putInt(FIELD_MIN_VOLUME, minVolume);
+ bundle.putInt(FIELD_MAX_VOLUME, maxVolume);
+ return bundle;
+ }
+
+ /** Creates an {@link DeviceInfo} instance from a {@link Bundle}. */
+ public static DeviceInfo fromBundle(Bundle bundle) {
+ int playbackType = bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
+ int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0);
+ int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0);
+ return new DeviceInfo(playbackType, minVolume, maxVolume);
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
index 0a5526e44d..3d35c6ad54 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
@@ -15,8 +15,9 @@
*/
package com.google.android.exoplayer2.device;
-// TODO(b/172315872) change back to @link after player migration to common.
-/** A listener for changes of {@code Player.DeviceComponent}. */
+import com.google.android.exoplayer2.Player;
+
+/** A listener for changes of {@link Player.DeviceComponent}. */
public interface DeviceListener {
/** Called when the device information changes. */
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SlowMotionData.java b/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SlowMotionData.java
index 4b8ed859a9..ae8698b66a 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SlowMotionData.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/metadata/mp4/SlowMotionData.java
@@ -15,13 +15,17 @@
*/
package com.google.android.exoplayer2.metadata.mp4;
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Objects;
+import com.google.common.collect.ComparisonChain;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
/** Holds information about the segments of slow motion playback within a track. */
@@ -30,6 +34,14 @@ public final class SlowMotionData implements Metadata.Entry {
/** Holds information about a single segment of slow motion playback within a track. */
public static final class Segment implements Parcelable {
+ public static final Comparator BY_START_THEN_END_THEN_DIVISOR =
+ (s1, s2) ->
+ ComparisonChain.start()
+ .compare(s1.startTimeMs, s2.startTimeMs)
+ .compare(s1.endTimeMs, s2.endTimeMs)
+ .compare(s1.speedDivisor, s2.speedDivisor)
+ .result();
+
/** The start time, in milliseconds, of the track segment that is intended to be slow motion. */
public final long startTimeMs;
/** The end time, in milliseconds, of the track segment that is intended to be slow motion. */
@@ -45,11 +57,12 @@ public final class SlowMotionData implements Metadata.Entry {
/**
* Creates an instance.
*
- * @param startTimeMs See {@link #startTimeMs}.
+ * @param startTimeMs See {@link #startTimeMs}. Must be less than endTimeMs.
* @param endTimeMs See {@link #endTimeMs}.
* @param speedDivisor See {@link #speedDivisor}.
*/
public Segment(long startTimeMs, long endTimeMs, int speedDivisor) {
+ checkArgument(startTimeMs < endTimeMs);
this.startTimeMs = startTimeMs;
this.endTimeMs = endTimeMs;
this.speedDivisor = speedDivisor;
@@ -113,9 +126,15 @@ public final class SlowMotionData implements Metadata.Entry {
public final List segments;
- /** Creates an instance with a list of {@link Segment}s. */
+ /**
+ * Creates an instance with a list of {@link Segment}s.
+ *
+ * The segments must not overlap, that is that the start time of a segment can not be between
+ * the start and end time of another segment.
+ */
public SlowMotionData(List segments) {
this.segments = segments;
+ checkArgument(!doSegmentsOverlap(segments));
}
@Override
@@ -164,4 +183,19 @@ public final class SlowMotionData implements Metadata.Entry {
return new SlowMotionData[size];
}
};
+
+ private static boolean doSegmentsOverlap(List segments) {
+ if (segments.isEmpty()) {
+ return false;
+ }
+ long previousEndTimeMs = segments.get(0).endTimeMs;
+ for (int i = 1; i < segments.size(); i++) {
+ if (segments.get(i).startTimeMs < previousEndTimeMs) {
+ return true;
+ }
+ previousEndTimeMs = segments.get(i).endTimeMs;
+ }
+
+ return false;
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
similarity index 86%
rename from library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
rename to library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
index 9e837bf05d..607f797103 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java
@@ -23,20 +23,10 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays;
-// TODO: Add an allowMultipleStreams boolean to indicate where the one stream per group restriction
-// does not apply.
-/**
- * Defines a group of tracks exposed by a {@link MediaPeriod}.
- *
- * A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a
- * group at any given time, however this {@link SampleStream} may adapt between multiple tracks
- * within the group.
- */
+/** Defines an immutable group of tracks identified by their format identity. */
public final class TrackGroup implements Parcelable {
- /**
- * The number of tracks in the group.
- */
+ /** The number of tracks in the group. */
public final int length;
private final Format[] formats;
@@ -45,7 +35,7 @@ public final class TrackGroup implements Parcelable {
private int hashCode;
/**
- * @param formats The track formats. Must not be null, contain null elements or be of length 0.
+ * @param formats The track formats. At least one {@link Format} must be provided.
*/
public TrackGroup(Format... formats) {
Assertions.checkState(formats.length > 0);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
similarity index 90%
rename from library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
rename to library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
index e737a5fafa..8db7b9c385 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java
@@ -21,17 +21,13 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.util.Arrays;
-/** An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}. */
+/** An immutable array of {@link TrackGroup}s. */
public final class TrackGroupArray implements Parcelable {
- /**
- * The empty array.
- */
+ /** The empty array. */
public static final TrackGroupArray EMPTY = new TrackGroupArray();
- /**
- * The number of groups in the array. Greater than or equal to zero.
- */
+ /** The number of groups in the array. Greater than or equal to zero. */
public final int length;
private final TrackGroup[] trackGroups;
@@ -39,9 +35,7 @@ public final class TrackGroupArray implements Parcelable {
// Lazily initialized hashcode.
private int hashCode;
- /**
- * @param trackGroups The groups. Must not be null or contain null elements, but may be empty.
- */
+ /** @param trackGroups The groups. May be empty. */
public TrackGroupArray(TrackGroup... trackGroups) {
this.trackGroups = trackGroups;
this.length = trackGroups.length;
@@ -83,9 +77,7 @@ public final class TrackGroupArray implements Parcelable {
return C.INDEX_UNSET;
}
- /**
- * Returns whether this track group array is empty.
- */
+ /** Returns whether this track group array is empty. */
public boolean isEmpty() {
return length == 0;
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
new file mode 100644
index 0000000000..dca840790d
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+
+/**
+ * A track selection consisting of a static subset of selected tracks belonging to a {@link
+ * TrackGroup}.
+ *
+ *
Tracks belonging to the subset are exposed in decreasing bandwidth order.
+ */
+public interface TrackSelection {
+
+ /** Returns the {@link TrackGroup} to which the selected tracks belong. */
+ TrackGroup getTrackGroup();
+
+ // Static subset of selected tracks.
+
+ /** Returns the number of tracks in the selection. */
+ int length();
+
+ /**
+ * Returns the format of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The format of the selected track.
+ */
+ Format getFormat(int index);
+
+ /**
+ * Returns the index in the track group of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The index of the selected track.
+ */
+ int getIndexInTrackGroup(int index);
+
+ /**
+ * Returns the index in the selection of the track with the specified format. The format is
+ * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) ==
+ * index} even if multiple selected tracks have formats that contain the same values.
+ *
+ * @param format The format.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * format is not part of the selection.
+ */
+ int indexOf(Format format);
+
+ /**
+ * Returns the index in the selection of the track with the specified index in the track group.
+ *
+ * @param indexInTrackGroup The index in the track group.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * index is not part of the selection.
+ */
+ int indexOf(int indexInTrackGroup);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
similarity index 99%
rename from library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
rename to library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
index fc20e863ba..b703998b2e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
@@ -73,5 +73,4 @@ public final class TrackSelectionArray {
TrackSelectionArray other = (TrackSelectionArray) obj;
return Arrays.equals(trackSelections, other.trackSelections);
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java
rename to library/common/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
similarity index 95%
rename from library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
rename to library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
index 9da576ea18..575a10b6cd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.upstream;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -24,7 +26,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
-import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate;
@@ -42,10 +43,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}.
@@ -208,7 +209,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
private static final long MAX_BYTES_TO_DRAIN = 2048;
private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
- private static final AtomicReference skipBufferReference = new AtomicReference<>();
private final boolean allowCrossProtocolRedirects;
private final int connectTimeoutMillis;
@@ -221,6 +221,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
@Nullable private DataSpec dataSpec;
@Nullable private HttpURLConnection connection;
@Nullable private InputStream inputStream;
+ private byte @MonotonicNonNull [] skipBuffer;
private boolean opened;
private int responseCode;
@@ -318,14 +319,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
@Override
public void setRequestProperty(String name, String value) {
- Assertions.checkNotNull(name);
- Assertions.checkNotNull(value);
+ checkNotNull(name);
+ checkNotNull(value);
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(String name) {
- Assertions.checkNotNull(name);
+ checkNotNull(name);
requestProperties.remove(name);
}
@@ -343,6 +344,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
this.bytesRead = 0;
this.bytesSkipped = 0;
transferInitializing(dataSpec);
+
try {
connection = makeConnection(dataSpec);
} catch (IOException e) {
@@ -355,6 +357,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
+ HttpURLConnection connection = this.connection;
String responseMessage;
try {
responseCode = connection.getResponseCode();
@@ -438,19 +441,22 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
skipInternal();
return readInternal(buffer, offset, readLength);
} catch (IOException e) {
- throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ);
+ throw new HttpDataSourceException(
+ e, castNonNull(dataSpec), HttpDataSourceException.TYPE_READ);
}
}
@Override
public void close() throws HttpDataSourceException {
try {
+ @Nullable InputStream inputStream = this.inputStream;
if (inputStream != null) {
maybeTerminateInputStream(connection, bytesRemaining());
try {
inputStream.close();
} catch (IOException e) {
- throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE);
+ throw new HttpDataSourceException(
+ e, castNonNull(dataSpec), HttpDataSourceException.TYPE_CLOSE);
}
}
} finally {
@@ -694,7 +700,9 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
if (matcher.find()) {
try {
long contentLengthFromRange =
- Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
+ Long.parseLong(checkNotNull(matcher.group(2)))
+ - Long.parseLong(checkNotNull(matcher.group(1)))
+ + 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
@@ -729,15 +737,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
return;
}
- // Acquire the shared skip buffer.
- byte[] skipBuffer = skipBufferReference.getAndSet(null);
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
while (bytesSkipped != bytesToSkip) {
int readLength = (int) min(bytesToSkip - bytesSkipped, skipBuffer.length);
- int read = inputStream.read(skipBuffer, 0, readLength);
+ int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
@@ -747,9 +753,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
bytesSkipped += read;
bytesTransferred(read);
}
-
- // Release the shared skip buffer.
- skipBufferReference.set(skipBuffer);
}
/**
@@ -778,7 +781,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
readLength = (int) min(readLength, bytesRemaining);
}
- int read = inputStream.read(buffer, offset, readLength);
+ int read = castNonNull(inputStream).read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
@@ -803,8 +806,9 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNSET} otherwise.
*/
- private static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) {
- if (Util.SDK_INT != 19 && Util.SDK_INT != 20) {
+ private static void maybeTerminateInputStream(
+ @Nullable HttpURLConnection connection, long bytesRemaining) {
+ if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
return;
}
@@ -825,7 +829,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|| "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream"
.equals(className)) {
Class> superclass = inputStream.getClass().getSuperclass();
- Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput");
+ Method unexpectedEndOfInput =
+ checkNotNull(superclass).getDeclaredMethod("unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
@@ -836,7 +841,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
}
}
-
/**
* Closes the current connection quietly, if there is one.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java
similarity index 87%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java
index ffb8236bd1..8ecb2ab8ec 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Clock.java
@@ -43,9 +43,6 @@ public interface Clock {
/** @see android.os.SystemClock#uptimeMillis() */
long uptimeMillis();
- /** @see android.os.SystemClock#sleep(long) */
- void sleep(long sleepTimeMs);
-
/**
* Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling
* messages.
@@ -53,4 +50,12 @@ public interface Clock {
* @see Handler#Handler(Looper, Handler.Callback)
*/
HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback);
+
+ /**
+ * Notifies the clock that the current thread is about to be blocked and won't return until a
+ * condition on another thread becomes true.
+ *
+ * Should be a no-op for all non-test cases.
+ */
+ void onThreadBlocked();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java
similarity index 100%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
similarity index 58%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
index 8343d27f42..637db2fe0d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java
@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util;
import android.os.Handler;
import android.os.Looper;
-import android.os.Message;
import androidx.annotation.Nullable;
/**
@@ -26,39 +25,55 @@ import androidx.annotation.Nullable;
*/
public interface HandlerWrapper {
- /** @see Handler#getLooper() */
+ /** A message obtained from the handler. */
+ interface Message {
+
+ /** See {@link android.os.Message#sendToTarget()}. */
+ void sendToTarget();
+
+ /** See {@link android.os.Message#getTarget()}. */
+ HandlerWrapper getTarget();
+ }
+
+ /** See {@link Handler#getLooper()}. */
Looper getLooper();
- /** @see Handler#obtainMessage(int) */
+ /** See {@link Handler#hasMessages(int)}. */
+ boolean hasMessages(int what);
+
+ /** See {@link Handler#obtainMessage(int)}. */
Message obtainMessage(int what);
- /** @see Handler#obtainMessage(int, Object) */
+ /** See {@link Handler#obtainMessage(int, Object)}. */
Message obtainMessage(int what, @Nullable Object obj);
- /** @see Handler#obtainMessage(int, int, int) */
+ /** See {@link Handler#obtainMessage(int, int, int)}. */
Message obtainMessage(int what, int arg1, int arg2);
- /** @see Handler#obtainMessage(int, int, int, Object) */
+ /** See {@link Handler#obtainMessage(int, int, int, Object)}. */
Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj);
- /** @see Handler#sendEmptyMessage(int) */
+ /** See {@link Handler#sendMessageAtFrontOfQueue(android.os.Message)}. */
+ boolean sendMessageAtFrontOfQueue(Message message);
+
+ /** See {@link Handler#sendEmptyMessage(int)}. */
boolean sendEmptyMessage(int what);
- /** @see Handler#sendEmptyMessageDelayed(int, long) */
+ /** See {@link Handler#sendEmptyMessageDelayed(int, long)}. */
boolean sendEmptyMessageDelayed(int what, int delayMs);
- /** @see Handler#sendEmptyMessageAtTime(int, long) */
+ /** See {@link Handler#sendEmptyMessageAtTime(int, long)}. */
boolean sendEmptyMessageAtTime(int what, long uptimeMs);
- /** @see Handler#removeMessages(int) */
+ /** See {@link Handler#removeMessages(int)}. */
void removeMessages(int what);
- /** @see Handler#removeCallbacksAndMessages(Object) */
+ /** See {@link Handler#removeCallbacksAndMessages(Object)}. */
void removeCallbacksAndMessages(@Nullable Object token);
- /** @see Handler#post(Runnable) */
+ /** See {@link Handler#post(Runnable)}. */
boolean post(Runnable runnable);
- /** @see Handler#postDelayed(Runnable, long) */
+ /** See {@link Handler#postDelayed(Runnable, long)}. */
boolean postDelayed(Runnable runnable, long delayMs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
similarity index 96%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
index d0df7a662e..a9a749e47f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.util;
-import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.CheckResult;
@@ -72,7 +71,8 @@ public final class ListenerSet {
private static final int MSG_ITERATION_FINISHED = 0;
private static final int MSG_LAZY_RELEASE = 1;
- private final Handler handler;
+ private final Clock clock;
+ private final HandlerWrapper handler;
private final Supplier eventFlagsSupplier;
private final IterationFinishedEvent iterationFinishedEvent;
private final CopyOnWriteArraySet> listeners;
@@ -86,6 +86,7 @@ public final class ListenerSet {
*
* @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used
* to call all other methods of this class.
+ * @param clock A {@link Clock}.
* @param eventFlagsSupplier A {@link Supplier} for new instances of {@link E the event flags
* type}.
* @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent
@@ -93,11 +94,13 @@ public final class ListenerSet {
*/
public ListenerSet(
Looper looper,
+ Clock clock,
Supplier eventFlagsSupplier,
IterationFinishedEvent iterationFinishedEvent) {
this(
/* listeners= */ new CopyOnWriteArraySet<>(),
looper,
+ clock,
eventFlagsSupplier,
iterationFinishedEvent);
}
@@ -105,8 +108,10 @@ public final class ListenerSet {
private ListenerSet(
CopyOnWriteArraySet> listeners,
Looper looper,
+ Clock clock,
Supplier eventFlagsSupplier,
IterationFinishedEvent iterationFinishedEvent) {
+ this.clock = clock;
this.listeners = listeners;
this.eventFlagsSupplier = eventFlagsSupplier;
this.iterationFinishedEvent = iterationFinishedEvent;
@@ -114,7 +119,7 @@ public final class ListenerSet {
queuedEvents = new ArrayDeque<>();
// It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("methodref.receiver.bound.invalid")
- Handler handler = Util.createHandler(looper, this::handleMessage);
+ HandlerWrapper handler = clock.createHandler(looper, this::handleMessage);
this.handler = handler;
}
@@ -129,7 +134,7 @@ public final class ListenerSet {
@CheckResult
public ListenerSet copy(
Looper looper, IterationFinishedEvent iterationFinishedEvent) {
- return new ListenerSet<>(listeners, looper, eventFlagsSupplier, iterationFinishedEvent);
+ return new ListenerSet<>(listeners, looper, clock, eventFlagsSupplier, iterationFinishedEvent);
}
/**
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Log.java
index e5e6f88d4d..fd1b74ca6e 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/Log.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Log.java
@@ -22,6 +22,7 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.UnknownHostException;
+import org.checkerframework.dataflow.qual.Pure;
/** Wrapper around {@link android.util.Log} which allows to set the log level. */
public final class Log {
@@ -51,11 +52,13 @@ public final class Log {
private Log() {}
/** Returns current {@link LogLevel} for ExoPlayer logcat logging. */
+ @Pure
public static @LogLevel int getLogLevel() {
return logLevel;
}
/** Returns whether stack traces of {@link Throwable}s will be logged to logcat. */
+ @Pure
public boolean getLogStackTraces() {
return logStackTraces;
}
@@ -80,6 +83,7 @@ public final class Log {
}
/** @see android.util.Log#d(String, String) */
+ @Pure
public static void d(String tag, String message) {
if (logLevel == LOG_LEVEL_ALL) {
android.util.Log.d(tag, message);
@@ -87,11 +91,13 @@ public final class Log {
}
/** @see android.util.Log#d(String, String, Throwable) */
+ @Pure
public static void d(String tag, String message, @Nullable Throwable throwable) {
d(tag, appendThrowableString(message, throwable));
}
/** @see android.util.Log#i(String, String) */
+ @Pure
public static void i(String tag, String message) {
if (logLevel <= LOG_LEVEL_INFO) {
android.util.Log.i(tag, message);
@@ -99,11 +105,13 @@ public final class Log {
}
/** @see android.util.Log#i(String, String, Throwable) */
+ @Pure
public static void i(String tag, String message, @Nullable Throwable throwable) {
i(tag, appendThrowableString(message, throwable));
}
/** @see android.util.Log#w(String, String) */
+ @Pure
public static void w(String tag, String message) {
if (logLevel <= LOG_LEVEL_WARNING) {
android.util.Log.w(tag, message);
@@ -111,11 +119,13 @@ public final class Log {
}
/** @see android.util.Log#w(String, String, Throwable) */
+ @Pure
public static void w(String tag, String message, @Nullable Throwable throwable) {
w(tag, appendThrowableString(message, throwable));
}
/** @see android.util.Log#e(String, String) */
+ @Pure
public static void e(String tag, String message) {
if (logLevel <= LOG_LEVEL_ERROR) {
android.util.Log.e(tag, message);
@@ -123,6 +133,7 @@ public final class Log {
}
/** @see android.util.Log#e(String, String, Throwable) */
+ @Pure
public static void e(String tag, String message, @Nullable Throwable throwable) {
e(tag, appendThrowableString(message, throwable));
}
@@ -139,6 +150,7 @@ public final class Log {
* @return The string representation of the {@link Throwable}.
*/
@Nullable
+ @Pure
public static String getThrowableString(@Nullable Throwable throwable) {
if (throwable == null) {
return null;
@@ -157,6 +169,7 @@ public final class Log {
}
}
+ @Pure
private static String appendThrowableString(String message, @Nullable Throwable throwable) {
@Nullable String throwableString = getThrowableString(throwable);
if (!TextUtils.isEmpty(throwableString)) {
@@ -165,6 +178,7 @@ public final class Log {
return message;
}
+ @Pure
private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) {
while (throwable != null) {
if (throwable instanceof UnknownHostException) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
similarity index 94%
rename from library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
rename to library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
index 89e1c60d7a..c3b31aa5c9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
@@ -43,13 +43,13 @@ public class SystemClock implements Clock {
return android.os.SystemClock.uptimeMillis();
}
- @Override
- public void sleep(long sleepTimeMs) {
- android.os.SystemClock.sleep(sleepTimeMs);
- }
-
@Override
public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) {
return new SystemHandlerWrapper(new Handler(looper, callback));
}
+
+ @Override
+ public void onThreadBlocked() {
+ // Do nothing.
+ }
}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
new file mode 100644
index 0000000000..a595245bc8
--- /dev/null
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
+import android.os.Handler;
+import android.os.Looper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+
+/** The standard implementation of {@link HandlerWrapper}. */
+/* package */ final class SystemHandlerWrapper implements HandlerWrapper {
+
+ private static final int MAX_POOL_SIZE = 50;
+
+ @GuardedBy("messagePool")
+ private static final List messagePool = new ArrayList<>(MAX_POOL_SIZE);
+
+ private final android.os.Handler handler;
+
+ public SystemHandlerWrapper(android.os.Handler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public Looper getLooper() {
+ return handler.getLooper();
+ }
+
+ @Override
+ public boolean hasMessages(int what) {
+ return handler.hasMessages(what);
+ }
+
+ @Override
+ public Message obtainMessage(int what) {
+ return obtainSystemMessage().setMessage(handler.obtainMessage(what), /* handler= */ this);
+ }
+
+ @Override
+ public Message obtainMessage(int what, @Nullable Object obj) {
+ return obtainSystemMessage().setMessage(handler.obtainMessage(what, obj), /* handler= */ this);
+ }
+
+ @Override
+ public Message obtainMessage(int what, int arg1, int arg2) {
+ return obtainSystemMessage()
+ .setMessage(handler.obtainMessage(what, arg1, arg2), /* handler= */ this);
+ }
+
+ @Override
+ public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) {
+ return obtainSystemMessage()
+ .setMessage(handler.obtainMessage(what, arg1, arg2, obj), /* handler= */ this);
+ }
+
+ @Override
+ public boolean sendMessageAtFrontOfQueue(Message message) {
+ return ((SystemMessage) message).sendAtFrontOfQueue(handler);
+ }
+
+ @Override
+ public boolean sendEmptyMessage(int what) {
+ return handler.sendEmptyMessage(what);
+ }
+
+ @Override
+ public boolean sendEmptyMessageDelayed(int what, int delayMs) {
+ return handler.sendEmptyMessageDelayed(what, delayMs);
+ }
+
+ @Override
+ public boolean sendEmptyMessageAtTime(int what, long uptimeMs) {
+ return handler.sendEmptyMessageAtTime(what, uptimeMs);
+ }
+
+ @Override
+ public void removeMessages(int what) {
+ handler.removeMessages(what);
+ }
+
+ @Override
+ public void removeCallbacksAndMessages(@Nullable Object token) {
+ handler.removeCallbacksAndMessages(token);
+ }
+
+ @Override
+ public boolean post(Runnable runnable) {
+ return handler.post(runnable);
+ }
+
+ @Override
+ public boolean postDelayed(Runnable runnable, long delayMs) {
+ return handler.postDelayed(runnable, delayMs);
+ }
+
+ private static SystemMessage obtainSystemMessage() {
+ synchronized (messagePool) {
+ return messagePool.isEmpty()
+ ? new SystemMessage()
+ : messagePool.remove(messagePool.size() - 1);
+ }
+ }
+
+ private static void recycleMessage(SystemMessage message) {
+ synchronized (messagePool) {
+ if (messagePool.size() < MAX_POOL_SIZE) {
+ messagePool.add(message);
+ }
+ }
+ }
+
+ private static final class SystemMessage implements Message {
+
+ @Nullable private android.os.Message message;
+ @Nullable private SystemHandlerWrapper handler;
+
+ public SystemMessage setMessage(android.os.Message message, SystemHandlerWrapper handler) {
+ this.message = message;
+ this.handler = handler;
+ return this;
+ }
+
+ public boolean sendAtFrontOfQueue(Handler handler) {
+ boolean success = handler.sendMessageAtFrontOfQueue(checkNotNull(message));
+ recycle();
+ return success;
+ }
+
+ @Override
+ public void sendToTarget() {
+ checkNotNull(message).sendToTarget();
+ recycle();
+ }
+
+ @Override
+ public HandlerWrapper getTarget() {
+ return checkNotNull(handler);
+ }
+
+ private void recycle() {
+ message = null;
+ handler = null;
+ recycleMessage(this);
+ }
+ }
+}
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
index c4af6bd2fc..61907c5175 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -91,6 +91,7 @@ import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.DataFormatException;
+import java.util.zip.GZIPOutputStream;
import java.util.zip.Inflater;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@@ -2045,8 +2046,6 @@ public final class Util {
/** Returns a data URI with the specified MIME type and data. */
public static Uri getDataUriForString(String mimeType, String data) {
- // TODO(internal: b/169937045): For now we don't pass the URL_SAFE flag as DataSchemeDataSource
- // doesn't decode using it.
return Uri.parse(
"data:" + mimeType + ";base64," + Base64.encodeToString(data.getBytes(), Base64.NO_WRAP));
}
@@ -2085,7 +2084,7 @@ public final class Util {
/** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */
public static File createTempFile(Context context, String prefix) throws IOException {
- return File.createTempFile(prefix, null, context.getCacheDir());
+ return File.createTempFile(prefix, null, checkNotNull(context.getCacheDir()));
}
/**
@@ -2123,6 +2122,17 @@ public final class Util {
return initialValue;
}
+ /** Compresses {@code input} using gzip and returns the result in a newly allocated byte array. */
+ public static byte[] gzip(byte[] input) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ try (GZIPOutputStream os = new GZIPOutputStream(output)) {
+ os.write(input);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ return output.toByteArray();
+ }
+
/**
* Absolute get method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link
* ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by
@@ -2179,7 +2189,7 @@ public final class Util {
return getMobileNetworkType(networkInfo);
case ConnectivityManager.TYPE_ETHERNET:
return C.NETWORK_TYPE_ETHERNET;
- default: // VPN, Bluetooth, Dummy.
+ default:
return C.NETWORK_TYPE_OTHER;
}
}
@@ -2610,7 +2620,7 @@ public final class Util {
"hsn", "zh-hsn"
};
- // Legacy ("grandfathered") tags, replaced by modern equivalents (including macrolanguage)
+ // Legacy tags that have been replaced by modern equivalents (including macrolanguage)
// See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry.
private static final String[] isoLegacyTagReplacements =
new String[] {
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/audio/AudioAttributesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/audio/AudioAttributesTest.java
new file mode 100644
index 0000000000..4d8193e662
--- /dev/null
+++ b/library/common/src/test/java/com/google/android/exoplayer2/audio/AudioAttributesTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link AudioAttributes}. */
+@RunWith(AndroidJUnit4.class)
+public class AudioAttributesTest {
+
+ @Test
+ public void roundtripViaBundle_yieldsEqualInstance() {
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(C.CONTENT_TYPE_SONIFICATION)
+ .setFlags(C.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(C.USAGE_ALARM)
+ .setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM)
+ .build();
+
+ assertThat(AudioAttributes.fromBundle(audioAttributes.toBundle())).isEqualTo(audioAttributes);
+ }
+}
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/device/DeviceInfoTest.java b/library/common/src/test/java/com/google/android/exoplayer2/device/DeviceInfoTest.java
new file mode 100644
index 0000000000..d8a8e34818
--- /dev/null
+++ b/library/common/src/test/java/com/google/android/exoplayer2/device/DeviceInfoTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.device;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link DeviceInfo}. */
+@RunWith(AndroidJUnit4.class)
+public class DeviceInfoTest {
+
+ @Test
+ public void roundtripViaBundle_yieldsEqualInstance() {
+ DeviceInfo deviceInfo =
+ new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 1, /* maxVolume= */ 9);
+
+ assertThat(DeviceInfo.fromBundle(deviceInfo.toBundle())).isEqualTo(deviceInfo);
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java b/library/common/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java
similarity index 100%
rename from library/core/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java
rename to library/common/src/test/java/com/google/android/exoplayer2/upstream/BaseDataSourceTest.java
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java b/library/common/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java
new file mode 100644
index 0000000000..0fb570f8b9
--- /dev/null
+++ b/library/common/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv;
+import com.google.common.collect.ImmutableList;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link DefaultHttpDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultHttpDataSourceContractTest extends DataSourceContractTest {
+
+ @Rule public HttpDataSourceTestEnv httpDataSourceTestEnv = new HttpDataSourceTestEnv();
+
+ @Override
+ protected DataSource createDataSource() {
+ return new DefaultHttpDataSource.Factory().createDataSource();
+ }
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return httpDataSourceTestEnv.getServedResources();
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl());
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java b/library/common/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java
similarity index 100%
rename from library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java
rename to library/common/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceTest.java
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java
similarity index 100%
rename from library/core/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java
rename to library/common/src/test/java/com/google/android/exoplayer2/util/ConditionVariableTest.java
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java
similarity index 90%
rename from library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java
rename to library/common/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java
index 6526b4db22..7d0b77664d 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java
+++ b/library/common/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java
@@ -43,7 +43,8 @@ public class ListenerSetTest {
@Test
public void queueEvent_withoutFlush_sendsNoEvents() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener = mock(TestListener.class);
listenerSet.add(listener);
@@ -57,7 +58,8 @@ public class ListenerSetTest {
@Test
public void flushEvents_sendsPreviouslyQueuedEventsToAllListeners() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener1 = mock(TestListener.class);
TestListener listener2 = mock(TestListener.class);
listenerSet.add(listener1);
@@ -81,7 +83,8 @@ public class ListenerSetTest {
@Test
public void flushEvents_recursive_sendsEventsInCorrectOrder() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
// Listener1 sends callback3 recursively when receiving callback1.
TestListener listener1 =
spy(
@@ -114,7 +117,8 @@ public class ListenerSetTest {
public void
flushEvents_withMultipleMessageQueueIterations_sendsIterationFinishedEventPerIteration() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
// Listener1 sends callback1 recursively when receiving callback3.
TestListener listener1 =
spy(
@@ -170,7 +174,8 @@ public class ListenerSetTest {
@Test
public void flushEvents_calledFromIterationFinishedCallback_restartsIterationFinishedEvents() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
// Listener2 sends callback1 recursively when receiving the iteration finished event.
TestListener listener2 =
spy(
@@ -212,7 +217,8 @@ public class ListenerSetTest {
@Test
public void flushEvents_withUnsetEventFlag_doesNotThrow() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
listenerSet.queueEvent(/* eventFlag= */ C.INDEX_UNSET, TestListener::callback1);
listenerSet.flushEvents();
@@ -224,7 +230,8 @@ public class ListenerSetTest {
@Test
public void add_withRecursion_onlyReceivesUpdatesForFutureEvents() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener2 = mock(TestListener.class);
// Listener1 adds listener2 recursively.
TestListener listener1 =
@@ -256,7 +263,8 @@ public class ListenerSetTest {
@Test
public void add_withQueueing_onlyReceivesUpdatesForFutureEvents() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener1 = mock(TestListener.class);
TestListener listener2 = mock(TestListener.class);
@@ -281,7 +289,8 @@ public class ListenerSetTest {
@Test
public void remove_withRecursion_stopsReceivingEventsImmediately() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener2 = mock(TestListener.class);
// Listener1 removes listener2 recursively.
TestListener listener1 =
@@ -309,7 +318,8 @@ public class ListenerSetTest {
@Test
public void remove_withQueueing_stopsReceivingEventsImmediately() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener1 = mock(TestListener.class);
TestListener listener2 = mock(TestListener.class);
listenerSet.add(listener1);
@@ -330,7 +340,8 @@ public class ListenerSetTest {
@Test
public void release_stopsForwardingEventsImmediately() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener2 = mock(TestListener.class);
// Listener1 releases the set from within the callback.
TestListener listener1 =
@@ -357,7 +368,8 @@ public class ListenerSetTest {
@Test
public void release_preventsRegisteringNewListeners() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener = mock(TestListener.class);
listenerSet.release();
@@ -370,7 +382,8 @@ public class ListenerSetTest {
@Test
public void lazyRelease_stopsForwardingEventsFromNewHandlerMessagesAndCallsReleaseCallback() {
ListenerSet listenerSet =
- new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished);
+ new ListenerSet<>(
+ Looper.myLooper(), Clock.DEFAULT, Flags::new, TestListener::iterationFinished);
TestListener listener = mock(TestListener.class);
listenerSet.add(listener);
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
index 2e5236a8f9..d4aaa869f3 100644
--- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
+++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchFloor;
import static com.google.android.exoplayer2.util.Util.escapeFileName;
import static com.google.android.exoplayer2.util.Util.getCodecsOfType;
import static com.google.android.exoplayer2.util.Util.getStringForTime;
+import static com.google.android.exoplayer2.util.Util.gzip;
import static com.google.android.exoplayer2.util.Util.minValue;
import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration;
@@ -37,6 +38,9 @@ import android.text.style.UnderlineSpan;
import android.util.SparseLongArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
@@ -45,6 +49,7 @@ import java.util.Formatter;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.zip.Deflater;
+import java.util.zip.GZIPInputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@@ -927,6 +932,17 @@ public class UtilTest {
assertThat(result).isEqualTo(0x4);
}
+ @Test
+ public void gzip_resultInflatesBackToOriginalValue() throws Exception {
+ byte[] input = TestUtil.buildTestData(20);
+
+ byte[] deflated = gzip(input);
+
+ byte[] inflated =
+ ByteStreams.toByteArray(new GZIPInputStream(new ByteArrayInputStream(deflated)));
+ assertThat(inflated).isEqualTo(input);
+ }
+
@Test
public void getBigEndianInt_fromBigEndian() {
byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C};
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 e4b6edba5b..2692925333 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
@@ -21,16 +21,14 @@ import static java.lang.Math.min;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
-/**
- * The default {@link LoadControl} implementation.
- */
+/** The default {@link LoadControl} implementation. */
public class DefaultLoadControl implements LoadControl {
/**
@@ -318,8 +316,8 @@ public class DefaultLoadControl implements LoadControl {
}
@Override
- public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
- TrackSelectionArray trackSelections) {
+ public void onTracksSelected(
+ Renderer[] renderers, TrackGroupArray trackGroups, ExoTrackSelection[] trackSelections) {
targetBufferBytes =
targetBufferBytesOverwrite == C.LENGTH_UNSET
? calculateTargetBufferBytes(renderers, trackSelections)
@@ -402,10 +400,10 @@ public class DefaultLoadControl implements LoadControl {
* @return The target buffer size in bytes.
*/
protected int calculateTargetBufferBytes(
- Renderer[] renderers, TrackSelectionArray trackSelectionArray) {
+ Renderer[] renderers, ExoTrackSelection[] trackSelectionArray) {
int targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) {
- if (trackSelectionArray.get(i) != null) {
+ if (trackSelectionArray[i] != null) {
targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType());
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 66c5c30d2a..9169271d12 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -449,13 +449,18 @@ public interface ExoPlayer extends Player {
}
}
- @Override
+ /**
+ * Returns the track selector that this player uses, or null if track selection is not supported.
+ */
@Nullable
TrackSelector getTrackSelector();
/** Returns the {@link Looper} associated with the playback thread. */
Looper getPlaybackLooper();
+ /** Returns the {@link Clock} used for playback. */
+ Clock getClock();
+
/** @deprecated Use {@link #prepare()} instead. */
@Deprecated
void retry();
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 d8e6affb2b..de8aa48891 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
@@ -34,13 +34,14 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@@ -67,10 +68,9 @@ import java.util.List;
private final Renderer[] renderers;
private final TrackSelector trackSelector;
- private final Handler playbackInfoUpdateHandler;
+ private final HandlerWrapper playbackInfoUpdateHandler;
private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener;
private final ExoPlayerImplInternal internalPlayer;
- private final Handler internalPlayerHandler;
private final ListenerSet listeners;
private final Timeline.Period period;
private final List mediaSourceHolderSnapshots;
@@ -79,6 +79,7 @@ import java.util.List;
@Nullable private final AnalyticsCollector analyticsCollector;
private final Looper applicationLooper;
private final BandwidthMeter bandwidthMeter;
+ private final Clock clock;
@RepeatMode private int repeatMode;
private boolean shuffleModeEnabled;
@@ -137,8 +138,15 @@ import java.util.List;
Clock clock,
Looper applicationLooper,
@Nullable Player wrappingPlayer) {
- Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
- + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
+ Log.i(
+ TAG,
+ "Init "
+ + Integer.toHexString(System.identityHashCode(this))
+ + " ["
+ + ExoPlayerLibraryInfo.VERSION_SLASHY
+ + "] ["
+ + Util.DEVICE_DEBUG_INFO
+ + "]");
checkState(renderers.length > 0);
this.renderers = checkNotNull(renderers);
this.trackSelector = checkNotNull(trackSelector);
@@ -149,11 +157,13 @@ import java.util.List;
this.seekParameters = seekParameters;
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
this.applicationLooper = applicationLooper;
+ this.clock = clock;
repeatMode = Player.REPEAT_MODE_OFF;
Player playerForListeners = wrappingPlayer != null ? wrappingPlayer : this;
listeners =
new ListenerSet<>(
applicationLooper,
+ clock,
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(playerForListeners, eventFlags));
mediaSourceHolderSnapshots = new ArrayList<>();
@@ -161,11 +171,11 @@ import java.util.List;
emptyTrackSelectorResult =
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
- new TrackSelection[renderers.length],
+ new ExoTrackSelection[renderers.length],
/* info= */ null);
period = new Timeline.Period();
maskingWindowIndex = C.INDEX_UNSET;
- playbackInfoUpdateHandler = new Handler(applicationLooper);
+ playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null);
playbackInfoUpdateListener =
playbackInfoUpdate ->
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
@@ -192,7 +202,6 @@ import java.util.List;
applicationLooper,
clock,
playbackInfoUpdateListener);
- internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
}
/**
@@ -259,6 +268,11 @@ import java.util.List;
return applicationLooper;
}
+ @Override
+ public Clock getClock() {
+ return clock;
+ }
+
@Override
public void addListener(Player.EventListener listener) {
listeners.add(listener);
@@ -724,9 +738,17 @@ import java.util.List;
@Override
public void release() {
- Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
- + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
- + ExoPlayerLibraryInfo.registeredModules() + "]");
+ Log.i(
+ TAG,
+ "Release "
+ + Integer.toHexString(System.identityHashCode(this))
+ + " ["
+ + ExoPlayerLibraryInfo.VERSION_SLASHY
+ + "] ["
+ + Util.DEVICE_DEBUG_INFO
+ + "] ["
+ + ExoPlayerLibraryInfo.registeredModules()
+ + "]");
if (!internalPlayer.release()) {
// One of the renderers timed out releasing its resources.
listeners.sendEvent(
@@ -754,7 +776,8 @@ import java.util.List;
target,
playbackInfo.timeline,
getCurrentWindowIndex(),
- internalPlayerHandler);
+ clock,
+ internalPlayer.getPlaybackLooper());
}
@Override
@@ -882,7 +905,7 @@ import java.util.List;
@Override
public TrackSelectionArray getCurrentTrackSelections() {
- return playbackInfo.trackSelectorResult.selections;
+ return new TrackSelectionArray(playbackInfo.trackSelectorResult.selections);
}
@Override
@@ -1005,11 +1028,11 @@ import java.util.List;
}
if (previousPlaybackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult) {
trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info);
+ TrackSelectionArray newSelection =
+ new TrackSelectionArray(newPlaybackInfo.trackSelectorResult.selections);
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
- listener ->
- listener.onTracksChanged(
- newPlaybackInfo.trackGroups, newPlaybackInfo.trackSelectorResult.selections));
+ listener -> listener.onTracksChanged(newPlaybackInfo.trackGroups, newSelection));
}
if (!previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
listeners.queueEvent(
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 dab8daa3c1..5a2c783a6f 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
@@ -40,8 +40,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
@@ -558,7 +557,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (e.isRecoverable && pendingRecoverableError == null) {
Log.w(TAG, "Recoverable playback error", e);
pendingRecoverableError = e;
- Message message = handler.obtainMessage(MSG_ATTEMPT_ERROR_RECOVERY, e);
+ HandlerWrapper.Message message = handler.obtainMessage(MSG_ATTEMPT_ERROR_RECOVERY, e);
// Given that the player is now in an unhandled exception state, the error needs to be
// recovered or the player stopped before any other message is handled.
message.getTarget().sendMessageAtFrontOfQueue(message);
@@ -625,6 +624,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean wasInterrupted = false;
while (!condition.get() && remainingMs > 0) {
try {
+ clock.onThreadBlocked();
wait(remainingMs);
} catch (InterruptedException e) {
wasInterrupted = true;
@@ -726,8 +726,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void notifyTrackSelectionPlayWhenReadyChanged(boolean playWhenReady) {
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
while (periodHolder != null) {
- TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : periodHolder.getTrackSelectorResult().selections) {
if (trackSelection != null) {
trackSelection.onPlayWhenReadyChanged(playWhenReady);
}
@@ -901,8 +900,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void notifyTrackSelectionRebuffer() {
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
while (periodHolder != null) {
- TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : periodHolder.getTrackSelectorResult().selections) {
if (trackSelection != null) {
trackSelection.onRebuffer();
}
@@ -1468,7 +1466,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException {
- if (message.getHandler().getLooper() == playbackLooper) {
+ if (message.getLooper() == playbackLooper) {
deliverMessage(message);
if (playbackInfo.playbackState == Player.STATE_READY
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
@@ -1481,21 +1479,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private void sendMessageToTargetThread(final PlayerMessage message) {
- Handler handler = message.getHandler();
- if (!handler.getLooper().getThread().isAlive()) {
+ Looper looper = message.getLooper();
+ if (!looper.getThread().isAlive()) {
Log.w("TAG", "Trying to send message on a dead thread.");
message.markAsProcessed(/* isDelivered= */ false);
return;
}
- handler.post(
- () -> {
- try {
- deliverMessage(message);
- } catch (ExoPlaybackException e) {
- Log.e(TAG, "Unexpected error delivering message on external thread.", e);
- throw new RuntimeException(e);
- }
- });
+ clock
+ .createHandler(looper, /* callback= */ null)
+ .post(
+ () -> {
+ try {
+ deliverMessage(message);
+ } catch (ExoPlaybackException e) {
+ Log.e(TAG, "Unexpected error delivering message on external thread.", e);
+ throw new RuntimeException(e);
+ }
+ });
}
private void deliverMessage(PlayerMessage message) throws ExoPlaybackException {
@@ -1690,8 +1690,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
while (periodHolder != null) {
- TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : periodHolder.getTrackSelectorResult().selections) {
if (trackSelection != null) {
trackSelection.onPlaybackSpeed(playbackSpeed);
}
@@ -1703,8 +1702,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void notifyTrackSelectionDiscontinuity() {
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
while (periodHolder != null) {
- TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll();
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : periodHolder.getTrackSelectorResult().selections) {
if (trackSelection != null) {
trackSelection.onDiscontinuity();
}
@@ -1732,8 +1730,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
: C.TIME_UNSET;
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
- boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
- return bufferedToEnd
+ boolean isBufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
+ // Ad loader implementations may only load ad media once playback has nearly reached the ad, but
+ // it is possible for playback to be stuck buffering waiting for this. Therefore, we start
+ // playback regardless of buffered duration if we are waiting for an ad media period to prepare.
+ boolean isAdPendingPreparation = loadingHolder.info.id.isAd() && !loadingHolder.prepared;
+ return isBufferedToEnd
+ || isAdPendingPreparation
|| loadControl.shouldStartPlayback(
getTotalBufferedDurationUs(),
mediaClock.getPlaybackParameters().speed,
@@ -2016,7 +2019,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
if (!renderer.isCurrentStreamFinal()) {
// The renderer stream is not final, so we can replace the sample streams immediately.
- Format[] formats = getFormats(newTrackSelectorResult.selections.get(i));
+ Format[] formats = getFormats(newTrackSelectorResult.selections[i]);
renderer.replaceStream(
formats,
readingPeriodHolder.sampleStreams[i],
@@ -2266,11 +2269,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
private ImmutableList extractMetadataFromTrackSelectionArray(
- TrackSelectionArray trackSelectionArray) {
+ ExoTrackSelection[] trackSelections) {
ImmutableList.Builder result = new ImmutableList.Builder<>();
boolean seenNonEmptyMetadata = false;
- for (int i = 0; i < trackSelectionArray.length; i++) {
- @Nullable TrackSelection trackSelection = trackSelectionArray.get(i);
+ for (ExoTrackSelection trackSelection : trackSelections) {
if (trackSelection != null) {
Format format = trackSelection.getFormat(/* index= */ 0);
if (format.metadata == null) {
@@ -2318,7 +2320,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();
RendererConfiguration rendererConfiguration =
trackSelectorResult.rendererConfigurations[rendererIndex];
- TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex);
+ ExoTrackSelection newSelection = trackSelectorResult.selections[rendererIndex];
Format[] formats = getFormats(newSelection);
// The renderer needs enabling with its new track selection.
boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY;
@@ -2792,7 +2794,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex);
}
- private static Format[] getFormats(TrackSelection newSelection) {
+ private static Format[] getFormats(ExoTrackSelection newSelection) {
// Build an array of formats contained by the selection.
int length = newSelection != null ? newSelection.length() : 0;
Format[] formats = new Format[length];
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoTimeoutException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoTimeoutException.java
index a6af2d193c..c35f6fa95e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoTimeoutException.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoTimeoutException.java
@@ -41,11 +41,9 @@ public final class ExoTimeoutException extends Exception {
/** The operation where this error occurred is not defined. */
public static final int TIMEOUT_OPERATION_UNDEFINED = 0;
- // TODO(b/172315872) Change back @code to @link when the Player is in common.
- /** The error occurred in {@code Player#release}. */
+ /** The error occurred in {@link Player#release}. */
public static final int TIMEOUT_OPERATION_RELEASE = 1;
- /** The error occurred in {@code ExoPlayer#setForegroundMode}. */
- // TODO(b/172315872) Set foregroundMode is an ExoPlayer method, NOT a player one.
+ /** The error occurred in {@link ExoPlayer#setForegroundMode}. */
public static final int TIMEOUT_OPERATION_SET_FOREGROUND_MODE = 2;
/** The error occurred while detaching a surface from the player. */
public static final int TIMEOUT_OPERATION_DETACH_SURFACE = 3;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
index f04ae8027d..66fa7a7f17 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
@@ -17,12 +17,10 @@ package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
-/**
- * Controls buffering of media.
- */
+/** Controls buffering of media. */
public interface LoadControl {
/** Called by the player when prepared with a new source. */
@@ -35,33 +33,27 @@ public interface LoadControl {
* @param trackGroups The {@link TrackGroup}s from which the selection was made.
* @param trackSelections The track selections that were made.
*/
- void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
- TrackSelectionArray trackSelections);
+ void onTracksSelected(
+ Renderer[] renderers, TrackGroupArray trackGroups, ExoTrackSelection[] trackSelections);
- /**
- * Called by the player when stopped.
- */
+ /** Called by the player when stopped. */
void onStopped();
- /**
- * Called by the player when released.
- */
+ /** Called by the player when released. */
void onReleased();
- /**
- * Returns the {@link Allocator} that should be used to obtain media buffer allocations.
- */
+ /** Returns the {@link Allocator} that should be used to obtain media buffer allocations. */
Allocator getAllocator();
/**
* Returns the duration of media to retain in the buffer prior to the current playback position,
* for fast backward seeking.
- *
- * Note: If {@link #retainBackBufferFromKeyframe()} is false then seeking in the back-buffer will
- * only be fast if the back-buffer contains a keyframe prior to the seek position.
- *
- * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not
- * currently supported.
+ *
+ *
Note: If {@link #retainBackBufferFromKeyframe()} is false then seeking in the back-buffer
+ * will only be fast if the back-buffer contains a keyframe prior to the seek position.
+ *
+ *
Note: Implementations should return a single value. Dynamic changes to the back-buffer are
+ * not currently supported.
*
* @return The duration of media to retain in the buffer prior to the current playback position,
* in microseconds.
@@ -71,17 +63,19 @@ public interface LoadControl {
/**
* Returns whether media should be retained from the keyframe before the current playback position
* minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position.
- *
- * Warning: Returning true will cause the back-buffer size to depend on the spacing of keyframes
- * in the media being played. Returning true is not recommended unless you control the media and
- * are comfortable with the back-buffer size exceeding {@link #getBackBufferDurationUs()} by as
- * much as the maximum duration between adjacent keyframes in the media.
- *
- * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not
- * currently supported.
+ *
+ *
Warning: Returning true will cause the back-buffer size to depend on the spacing of
+ * keyframes in the media being played. Returning true is not recommended unless you control the
+ * media and are comfortable with the back-buffer size exceeding {@link
+ * #getBackBufferDurationUs()} by as much as the maximum duration between adjacent keyframes in
+ * the media.
+ *
+ *
Note: Implementations should return a single value. Dynamic changes to the back-buffer are
+ * not currently supported.
*
* @return Whether media should be retained from the keyframe before the current playback position
- * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position.
+ * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that
+ * position.
*/
boolean retainBackBufferFromKeyframe();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
index 6bbd609dd5..e8639e1f9a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
@@ -24,8 +24,7 @@ import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -233,7 +232,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
throws ExoPlaybackException {
TrackSelectorResult selectorResult =
trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline);
- for (TrackSelection trackSelection : selectorResult.selections.getAll()) {
+ for (ExoTrackSelection trackSelection : selectorResult.selections) {
if (trackSelection != null) {
trackSelection.onPlaybackSpeed(playbackSpeed);
}
@@ -289,10 +288,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
trackSelectorResult = newTrackSelectorResult;
enableTrackSelectionsInResult();
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
- TrackSelectionArray trackSelections = newTrackSelectorResult.selections;
positionUs =
mediaPeriod.selectTracks(
- trackSelections.getAll(),
+ newTrackSelectorResult.selections,
mayRetainStreamFlags,
sampleStreams,
streamResetFlags,
@@ -309,7 +307,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
hasEnabledTracks = true;
}
} else {
- Assertions.checkState(trackSelections.get(i) == null);
+ Assertions.checkState(newTrackSelectorResult.selections[i] == null);
}
}
return positionUs;
@@ -361,7 +359,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
for (int i = 0; i < trackSelectorResult.length; i++) {
boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);
- TrackSelection trackSelection = trackSelectorResult.selections.get(i);
+ ExoTrackSelection trackSelection = trackSelectorResult.selections[i];
if (rendererEnabled && trackSelection != null) {
trackSelection.enable();
}
@@ -374,7 +372,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
for (int i = 0; i < trackSelectorResult.length; i++) {
boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i);
- TrackSelection trackSelection = trackSelectorResult.selections.get(i);
+ ExoTrackSelection trackSelection = trackSelectorResult.selections[i];
if (rendererEnabled && trackSelection != null) {
trackSelection.disable();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
index 6f81a35dd8..4191480700 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
@@ -16,8 +16,8 @@
package com.google.android.exoplayer2;
import android.os.Handler;
+import android.os.Looper;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import java.util.concurrent.TimeoutException;
@@ -55,11 +55,12 @@ public final class PlayerMessage {
private final Target target;
private final Sender sender;
+ private final Clock clock;
private final Timeline timeline;
private int type;
@Nullable private Object payload;
- private Handler handler;
+ private Looper looper;
private int windowIndex;
private long positionMs;
private boolean deleteAfterDelivery;
@@ -77,7 +78,8 @@ public final class PlayerMessage {
* set to {@link Timeline#EMPTY}, any position can be specified.
* @param defaultWindowIndex The default window index in the {@code timeline} when no other window
* index is specified.
- * @param defaultHandler The default handler to send the message on when no other handler is
+ * @param clock The {@link Clock}.
+ * @param defaultLooper The default {@link Looper} to send the message on when no other looper is
* specified.
*/
public PlayerMessage(
@@ -85,11 +87,13 @@ public final class PlayerMessage {
Target target,
Timeline timeline,
int defaultWindowIndex,
- Handler defaultHandler) {
+ Clock clock,
+ Looper defaultLooper) {
this.sender = sender;
this.target = target;
this.timeline = timeline;
- this.handler = defaultHandler;
+ this.looper = defaultLooper;
+ this.clock = clock;
this.windowIndex = defaultWindowIndex;
this.positionMs = C.TIME_UNSET;
this.deleteAfterDelivery = true;
@@ -142,22 +146,28 @@ public final class PlayerMessage {
return payload;
}
+ /** @deprecated Use {@link #setLooper(Looper)} instead. */
+ @Deprecated
+ public PlayerMessage setHandler(Handler handler) {
+ return setLooper(handler.getLooper());
+ }
+
/**
- * Sets the handler the message is delivered on.
+ * Sets the {@link Looper} the message is delivered on.
*
- * @param handler A {@link Handler}.
+ * @param looper A {@link Looper}.
* @return This message.
* @throws IllegalStateException If {@link #send()} has already been called.
*/
- public PlayerMessage setHandler(Handler handler) {
+ public PlayerMessage setLooper(Looper looper) {
Assertions.checkState(!isSent);
- this.handler = handler;
+ this.looper = looper;
return this;
}
- /** Returns the handler the message is delivered on. */
- public Handler getHandler() {
- return handler;
+ /** Returns the {@link Looper} the message is delivered on. */
+ public Looper getLooper() {
+ return looper;
}
/**
@@ -287,19 +297,19 @@ public final class PlayerMessage {
* Blocks until after the message has been delivered or the player is no longer able to deliver
* the message.
*
- *
Note that this method can't be called if the current thread is the same thread used by the
- * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock.
+ *
Note that this method must not be called if the current thread is the same thread used by
+ * the message {@link #getLooper() looper} as it would cause a deadlock.
*
* @return Whether the message was delivered successfully.
* @throws IllegalStateException If this method is called before {@link #send()}.
* @throws IllegalStateException If this method is called on the same thread used by the message
- * handler set with {@link #setHandler(Handler)}.
+ * {@link #getLooper() looper}.
* @throws InterruptedException If the current thread is interrupted while waiting for the message
* to be delivered.
*/
public synchronized boolean blockUntilDelivered() throws InterruptedException {
Assertions.checkState(isSent);
- Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread());
+ Assertions.checkState(looper.getThread() != Thread.currentThread());
while (!isProcessed) {
wait();
}
@@ -310,14 +320,14 @@ public final class PlayerMessage {
* Blocks until after the message has been delivered or the player is no longer able to deliver
* the message or the specified timeout elapsed.
*
- *
Note that this method can't be called if the current thread is the same thread used by the
- * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock.
+ *
Note that this method must not be called if the current thread is the same thread used by
+ * the message {@link #getLooper() looper} as it would cause a deadlock.
*
* @param timeoutMs The timeout in milliseconds.
* @return Whether the message was delivered successfully.
* @throws IllegalStateException If this method is called before {@link #send()}.
* @throws IllegalStateException If this method is called on the same thread used by the message
- * handler set with {@link #setHandler(Handler)}.
+ * {@link #getLooper() looper}.
* @throws TimeoutException If the {@code timeoutMs} elapsed and this message has not been
* delivered and the player is still able to deliver the message.
* @throws InterruptedException If the current thread is interrupted while waiting for the message
@@ -325,26 +335,19 @@ public final class PlayerMessage {
*/
public synchronized boolean blockUntilDelivered(long timeoutMs)
throws InterruptedException, TimeoutException {
- return blockUntilDelivered(timeoutMs, Clock.DEFAULT);
- }
-
- @VisibleForTesting()
- /* package */ synchronized boolean blockUntilDelivered(long timeoutMs, Clock clock)
- throws InterruptedException, TimeoutException {
Assertions.checkState(isSent);
- Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread());
+ Assertions.checkState(looper.getThread() != Thread.currentThread());
long deadlineMs = clock.elapsedRealtime() + timeoutMs;
long remainingMs = timeoutMs;
while (!isProcessed && remainingMs > 0) {
+ clock.onThreadBlocked();
wait(remainingMs);
remainingMs = deadlineMs - clock.elapsedRealtime();
}
-
if (!isProcessed) {
throw new TimeoutException("Message delivery timed out.");
}
-
return isDelivered;
}
}
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 01716fc0fb..e89e05eb64 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
@@ -1202,6 +1202,11 @@ public class SimpleExoPlayer extends BasePlayer
return player.getApplicationLooper();
}
+ @Override
+ public Clock getClock() {
+ return player.getClock();
+ }
+
@Override
public void addListener(Player.EventListener listener) {
// Don't verify application thread. We allow calls to this method from any thread.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
index e4705bd761..20bb920c57 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -91,6 +91,7 @@ public class AnalyticsCollector
listeners =
new ListenerSet<>(
Util.getCurrentOrMainLooper(),
+ clock,
AnalyticsListener.Events::new,
(listener, eventFlags) -> {});
period = new Period();
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 7bd7d1a7eb..8f16df115a 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
@@ -1457,28 +1457,69 @@ public final class DefaultAudioSink implements AudioSink {
if (!supportedEncoding) {
return null;
}
-
- // E-AC3 JOC is object based, so any channel count specified in the format is arbitrary. Use 6,
- // since the E-AC3 compatible part of the stream is 5.1.
- int channelCount = encoding == C.ENCODING_E_AC3_JOC ? 6 : format.channelCount;
- if (channelCount > audioCapabilities.getMaxChannelCount()) {
+ if (encoding == C.ENCODING_E_AC3_JOC
+ && !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) {
+ // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
+ encoding = C.ENCODING_E_AC3;
+ }
+ if (!audioCapabilities.supportsEncoding(encoding)) {
return null;
}
+ int channelCount;
+ if (encoding == C.ENCODING_E_AC3_JOC) {
+ // E-AC3 JOC is object based so the format channel count is arbitrary. From API 29 we can get
+ // the channel count for this encoding, but before then there is no way to query it so we
+ // assume 6 channel audio is supported.
+ if (Util.SDK_INT >= 29) {
+ channelCount =
+ getMaxSupportedChannelCountForPassthroughV29(C.ENCODING_E_AC3_JOC, format.sampleRate);
+ if (channelCount == 0) {
+ Log.w(TAG, "E-AC3 JOC encoding supported but no channel count supported");
+ return null;
+ }
+ } else {
+ channelCount = 6;
+ }
+ } else {
+ channelCount = format.channelCount;
+ if (channelCount > audioCapabilities.getMaxChannelCount()) {
+ return null;
+ }
+ }
int channelConfig = getChannelConfigForPassthrough(channelCount);
if (channelConfig == AudioFormat.CHANNEL_INVALID) {
return null;
}
- if (audioCapabilities.supportsEncoding(encoding)) {
- return Pair.create(encoding, channelConfig);
- } else if (encoding == C.ENCODING_E_AC3_JOC
- && audioCapabilities.supportsEncoding(C.ENCODING_E_AC3)) {
- // E-AC3 receivers support E-AC3 JOC streams (but decode in 2-D rather than 3-D).
- return Pair.create(C.ENCODING_E_AC3, channelConfig);
- }
+ return Pair.create(encoding, channelConfig);
+ }
- return null;
+ /**
+ * Returns the maximum number of channels supported for passthrough playback of audio in the given
+ * format, or 0 if the format is unsupported.
+ */
+ @RequiresApi(29)
+ private static int getMaxSupportedChannelCountForPassthroughV29(
+ @C.Encoding int encoding, int sampleRate) {
+ android.media.AudioAttributes audioAttributes =
+ new android.media.AudioAttributes.Builder()
+ .setUsage(android.media.AudioAttributes.USAGE_MEDIA)
+ .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE)
+ .build();
+ // TODO(internal b/25994457): Query supported channel masks directly once it's supported.
+ for (int channelCount = 8; channelCount > 0; channelCount--) {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(encoding)
+ .setSampleRate(sampleRate)
+ .setChannelMask(Util.getAudioTrackChannelConfig(channelCount))
+ .build();
+ if (AudioTrack.isDirectPlaybackSupported(audioFormat, audioAttributes)) {
+ return channelCount;
+ }
+ }
+ return 0;
}
private static int getChannelConfigForPassthrough(int channelCount) {
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 0cec4ab789..f7d7a097a0 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
@@ -26,6 +26,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Pair;
+import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
@@ -308,7 +309,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// Assigning null to various non-null variables for clean-up.
state = STATE_RELEASED;
Util.castNonNull(responseHandler).removeCallbacksAndMessages(null);
- Util.castNonNull(requestHandler).removeCallbacksAndMessages(null);
+ Util.castNonNull(requestHandler).release();
requestHandler = null;
Util.castNonNull(requestHandlerThread).quit();
requestHandlerThread = null;
@@ -570,6 +571,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@SuppressLint("HandlerLeak")
private class RequestHandler extends Handler {
+ @GuardedBy("this")
+ private boolean isReleased;
+
public RequestHandler(Looper backgroundLooper) {
super(backgroundLooper);
}
@@ -610,9 +614,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
response = e;
}
loadErrorHandlingPolicy.onLoadTaskConcluded(requestTask.taskId);
- responseHandler
- .obtainMessage(msg.what, Pair.create(requestTask.request, response))
- .sendToTarget();
+ synchronized (this) {
+ if (!isReleased) {
+ responseHandler
+ .obtainMessage(msg.what, Pair.create(requestTask.request, response))
+ .sendToTarget();
+ }
+ }
}
private boolean maybeRetryRequest(Message originalMsg, MediaDrmCallbackException exception) {
@@ -647,8 +655,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// The error is fatal.
return false;
}
- sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs);
- return true;
+ synchronized (this) {
+ if (!isReleased) {
+ sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public synchronized void release() {
+ removeCallbacksAndMessages(/* token= */ null);
+ isReleased = true;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index be02faeba8..67cb095b8d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -36,6 +36,7 @@ import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@@ -632,12 +633,12 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// ResourceBusyException is only available at API 19, so on earlier versions we always
// eagerly release regardless of the underlying error.
if (!keepaliveSessions.isEmpty()) {
- // Make a local copy, because sessions are removed from this.timingOutSessions during
+ // Make a local copy, because sessions are removed from this.keepaliveSessions during
// release (via callback).
- ImmutableList timingOutSessions =
- ImmutableList.copyOf(this.keepaliveSessions);
- for (DrmSession timingOutSession : timingOutSessions) {
- timingOutSession.release(/* eventDispatcher= */ null);
+ ImmutableSet keepaliveSessions =
+ ImmutableSet.copyOf(this.keepaliveSessions);
+ for (DrmSession keepaliveSession : keepaliveSessions) {
+ keepaliveSession.release(/* eventDispatcher= */ null);
}
// Undo the acquisitions from createAndAcquireSession().
session.release(eventDispatcher);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java
similarity index 86%
rename from library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java
rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java
index 0529ec3127..10bd2953d5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerProvider.java
@@ -13,16 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.android.exoplayer2.source;
+package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
-import com.google.android.exoplayer2.drm.DrmSessionManager;
-import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
-import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -31,8 +27,8 @@ import com.google.android.exoplayer2.util.Util;
import com.google.common.primitives.Ints;
import java.util.Map;
-/** A helper to create a {@link DrmSessionManager} from a {@link MediaItem}. */
-public final class MediaSourceDrmHelper {
+/** Default implementation of {@link DrmSessionManagerProvider}. */
+public final class DefaultDrmSessionManagerProvider implements DrmSessionManagerProvider {
@Nullable private HttpDataSource.Factory drmHttpDataSourceFactory;
@Nullable private String userAgent;
@@ -62,13 +58,13 @@ public final class MediaSourceDrmHelper {
this.userAgent = userAgent;
}
- /** Creates a {@link DrmSessionManager} for the given media item. */
- public DrmSessionManager create(MediaItem mediaItem) {
+ @Override
+ public DrmSessionManager get(MediaItem mediaItem) {
Assertions.checkNotNull(mediaItem.playbackProperties);
@Nullable
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
if (drmConfiguration == null || Util.SDK_INT < 18) {
- return DrmSessionManager.getDummyDrmSessionManager();
+ return DrmSessionManager.DRM_UNSUPPORTED;
}
HttpDataSource.Factory dataSourceFactory =
drmHttpDataSourceFactory != null
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
index 1168884d76..70dc4fa7f5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -22,13 +22,8 @@ import com.google.android.exoplayer2.Format;
/** Manages a DRM session. */
public interface DrmSessionManager {
- /** Returns {@link #DUMMY}. */
- static DrmSessionManager getDummyDrmSessionManager() {
- return DUMMY;
- }
-
- /** {@link DrmSessionManager} that supports no DRM schemes. */
- DrmSessionManager DUMMY =
+ /** An instance that supports no DRM schemes. */
+ DrmSessionManager DRM_UNSUPPORTED =
new DrmSessionManager() {
@Override
@@ -54,6 +49,23 @@ public interface DrmSessionManager {
}
};
+ /**
+ * An instance that supports no DRM schemes.
+ *
+ * @deprecated Use {@link #DRM_UNSUPPORTED}.
+ */
+ @Deprecated DrmSessionManager DUMMY = DRM_UNSUPPORTED;
+
+ /**
+ * Returns {@link #DRM_UNSUPPORTED}.
+ *
+ * @deprecated Use {@link #DRM_UNSUPPORTED}.
+ */
+ @Deprecated
+ static DrmSessionManager getDummyDrmSessionManager() {
+ return DRM_UNSUPPORTED;
+ }
+
/**
* Acquires any required resources.
*
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorInterface.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManagerProvider.java
similarity index 52%
rename from library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorInterface.java
rename to library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManagerProvider.java
index abd4b32480..9fa0d1a9c2 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorInterface.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManagerProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,14 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.android.exoplayer2.trackselection;
+package com.google.android.exoplayer2.drm;
+
+import com.google.android.exoplayer2.MediaItem;
-// TODO(b/172315872) Replace @code by @link when Player has been migrated to common
/**
- * The component of a {@code Player} responsible for selecting tracks to be played.
- *
- * No Player agnostic track selection is currently supported. Clients should downcast to the
- * implementation's track selection.
+ * A provider to obtain a {@link DrmSessionManager} suitable for playing the content described by a
+ * {@link MediaItem}.
*/
-// TODO(b/172315872) Define an interface for track selection.
-public interface TrackSelectorInterface {}
+public interface DrmSessionManagerProvider {
+
+ /** Returns a {@link DrmSessionManager} for the given media item. */
+ DrmSessionManager get(MediaItem mediaItem);
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/C2Mp3TimestampTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/C2Mp3TimestampTracker.java
index 0c3fe9facf..04b453d529 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/C2Mp3TimestampTracker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/C2Mp3TimestampTracker.java
@@ -30,7 +30,7 @@ import java.nio.ByteBuffer;
/* package */ final class C2Mp3TimestampTracker {
// Mirroring the actual codec, as can be found at
- // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/codec2/components/mp3/C2SoftMp3Dec.h;l=55;drc=3665390c9d32a917398b240c5a46ced07a3b65eb
+ // https://cs.android.com/android/platform/superproject/+/main:frameworks/av/media/codec2/components/mp3/C2SoftMp3Dec.h;l=55;drc=3665390c9d32a917398b240c5a46ced07a3b65eb
private static final long DECODER_DELAY_SAMPLES = 529;
private static final String TAG = "C2Mp3TimestampTracker";
@@ -76,7 +76,7 @@ import java.nio.ByteBuffer;
}
// These calculations mirror the timestamp calculations in the Codec2 Mp3 Decoder.
- // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/codec2/components/mp3/C2SoftMp3Dec.cpp;l=464;drc=ed134640332fea70ca4b05694289d91a5265bb46
+ // https://cs.android.com/android/platform/superproject/+/main:frameworks/av/media/codec2/components/mp3/C2SoftMp3Dec.cpp;l=464;drc=ed134640332fea70ca4b05694289d91a5265bb46
if (processedSamples == 0) {
anchorTimestampUs = buffer.timeUs;
processedSamples = frameCount - DECODER_DELAY_SAMPLES;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index 1b35fc9887..99396af2f4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -298,8 +298,16 @@ public final class MediaCodecInfo {
// which may not be widely supported. See https://github.com/google/ExoPlayer/issues/5145.
return true;
}
- for (CodecProfileLevel capabilities : getProfileLevels()) {
- if (capabilities.profile == profile && capabilities.level >= level) {
+
+ CodecProfileLevel[] profileLevels = getProfileLevels();
+ if (Util.SDK_INT <= 23 && MimeTypes.VIDEO_VP9.equals(mimeType) && profileLevels.length == 0) {
+ // Some older devices don't report profile levels for VP9. Estimate them using other data in
+ // the codec capabilities.
+ profileLevels = estimateLegacyVp9ProfileLevels(capabilities);
+ }
+
+ for (CodecProfileLevel profileLevel : profileLevels) {
+ if (profileLevel.profile == profile && profileLevel.level >= level) {
return true;
}
}
@@ -334,8 +342,8 @@ public final class MediaCodecInfo {
if (isVideo) {
return adaptive;
} else {
- Pair codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format);
- return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE;
+ Pair profileLevel = MediaCodecUtil.getCodecProfileAndLevel(format);
+ return profileLevel != null && profileLevel.first == CodecProfileLevel.AACObjectXHE;
}
}
@@ -678,6 +686,60 @@ public final class MediaCodecInfo {
return capabilities.getMaxSupportedInstances();
}
+ /**
+ * Called on devices with {@link Util#SDK_INT} 23 and below, for VP9 decoders whose {@link
+ * CodecCapabilities} do not correctly report profile levels. The returned {@link
+ * CodecProfileLevel CodecProfileLevels} are estimated based on other data in the {@link
+ * CodecCapabilities}.
+ *
+ * @param capabilities The {@link CodecCapabilities} for a VP9 decoder, or {@code null} if not
+ * known.
+ * @return The estimated {@link CodecProfileLevel CodecProfileLevels} for the decoder.
+ */
+ private static CodecProfileLevel[] estimateLegacyVp9ProfileLevels(
+ @Nullable CodecCapabilities capabilities) {
+ int maxBitrate = 0;
+ if (capabilities != null) {
+ @Nullable VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
+ if (videoCapabilities != null) {
+ maxBitrate = videoCapabilities.getBitrateRange().getUpper();
+ }
+ }
+
+ // Values taken from https://www.webmproject.org/vp9/levels.
+ int level;
+ if (maxBitrate >= 180_000_000) {
+ level = CodecProfileLevel.VP9Level52;
+ } else if (maxBitrate >= 120_000_000) {
+ level = CodecProfileLevel.VP9Level51;
+ } else if (maxBitrate >= 60_000_000) {
+ level = CodecProfileLevel.VP9Level5;
+ } else if (maxBitrate >= 30_000_000) {
+ level = CodecProfileLevel.VP9Level41;
+ } else if (maxBitrate >= 18_000_000) {
+ level = CodecProfileLevel.VP9Level4;
+ } else if (maxBitrate >= 12_000_000) {
+ level = CodecProfileLevel.VP9Level31;
+ } else if (maxBitrate >= 7_200_000) {
+ level = CodecProfileLevel.VP9Level3;
+ } else if (maxBitrate >= 3_600_000) {
+ level = CodecProfileLevel.VP9Level21;
+ } else if (maxBitrate >= 1_800_000) {
+ level = CodecProfileLevel.VP9Level2;
+ } else if (maxBitrate >= 800_000) {
+ level = CodecProfileLevel.VP9Level11;
+ } else { // Assume level 1 is always supported.
+ level = CodecProfileLevel.VP9Level1;
+ }
+
+ CodecProfileLevel profileLevel = new CodecProfileLevel();
+ // Since this method is for legacy devices only, assume that only profile 0 is supported.
+ profileLevel.profile = CodecProfileLevel.VP9Profile0;
+ profileLevel.level = level;
+
+ return new CodecProfileLevel[] {profileLevel};
+ }
+
/**
* Returns whether the decoder is known to fail when adapting, despite advertising itself as an
* adaptive decoder.
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 6ca35003d9..1e4506c795 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
@@ -2206,8 +2206,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (bypassBatchBuffer.hasSamples()) {
bypassBatchBuffer.flip();
}
- // We can make more progress if we have batched data or the EOS to process.
- return bypassBatchBuffer.hasSamples() || inputStreamEnded;
+
+ // We can make more progress if we have batched data, an EOS, or a re-initialization to process
+ // (note that one or more of the code blocks above will be executed during the next call).
+ return bypassBatchBuffer.hasSamples() || inputStreamEnded || bypassDrainAndReinitialize;
}
private void bypassRead() throws ExoPlaybackException {
@@ -2221,7 +2223,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
switch (result) {
case C.RESULT_FORMAT_READ:
onInputFormatChanged(formatHolder);
- break;
+ return;
case C.RESULT_NOTHING_READ:
return;
case C.RESULT_BUFFER_READ:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
index 60df8413b1..27ff0a7956 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
@@ -48,8 +48,8 @@ import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
@@ -465,8 +465,9 @@ public final class DownloadHelper {
private @MonotonicNonNull MediaPreparer mediaPreparer;
private TrackGroupArray @MonotonicNonNull [] trackGroupArrays;
private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos;
- private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
- private List @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer;
+ private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer;
+ private List @MonotonicNonNull [][]
+ immutableTrackSelectionsByPeriodAndRenderer;
/**
* Creates download helper.
@@ -573,14 +574,14 @@ public final class DownloadHelper {
}
/**
- * Returns all {@link TrackSelection track selections} for a period and renderer. Must not be
+ * Returns all {@link ExoTrackSelection track selections} for a period and renderer. Must not be
* called until after preparation completes.
*
* @param periodIndex The period index.
* @param rendererIndex The renderer index.
- * @return A list of selected {@link TrackSelection track selections}.
+ * @return A list of selected {@link ExoTrackSelection track selections}.
*/
- public List getTrackSelections(int periodIndex, int rendererIndex) {
+ public List getTrackSelections(int periodIndex, int rendererIndex) {
assertPreparedWithMedia();
return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex];
}
@@ -751,7 +752,7 @@ public final class DownloadHelper {
}
assertPreparedWithMedia();
List streamKeys = new ArrayList<>();
- List allSelections = new ArrayList<>();
+ List allSelections = new ArrayList<>();
int periodCount = trackSelectionsByPeriodAndRenderer.length;
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
allSelections.clear();
@@ -773,9 +774,9 @@ public final class DownloadHelper {
int periodCount = mediaPreparer.mediaPeriods.length;
int rendererCount = rendererCapabilities.length;
trackSelectionsByPeriodAndRenderer =
- (List[][]) new List>[periodCount][rendererCount];
+ (List[][]) new List>[periodCount][rendererCount];
immutableTrackSelectionsByPeriodAndRenderer =
- (List[][]) new List>[periodCount][rendererCount];
+ (List[][]) new List>[periodCount][rendererCount];
for (int i = 0; i < periodCount; i++) {
for (int j = 0; j < rendererCount; j++) {
trackSelectionsByPeriodAndRenderer[i][j] = new ArrayList<>();
@@ -847,15 +848,15 @@ public final class DownloadHelper {
new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
mediaPreparer.timeline);
for (int i = 0; i < trackSelectorResult.length; i++) {
- @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i);
+ @Nullable ExoTrackSelection newSelection = trackSelectorResult.selections[i];
if (newSelection == null) {
continue;
}
- List existingSelectionList =
+ List existingSelectionList =
trackSelectionsByPeriodAndRenderer[periodIndex][i];
boolean mergedWithExistingSelection = false;
for (int j = 0; j < existingSelectionList.size(); j++) {
- TrackSelection existingSelection = existingSelectionList.get(j);
+ ExoTrackSelection existingSelection = existingSelectionList.get(j);
if (existingSelection.getTrackGroup() == newSelection.getTrackGroup()) {
// Merge with existing selection.
scratchSet.clear();
@@ -1066,15 +1067,15 @@ public final class DownloadHelper {
private static final class DownloadTrackSelection extends BaseTrackSelection {
- private static final class Factory implements TrackSelection.Factory {
+ private static final class Factory implements ExoTrackSelection.Factory {
@Override
- public @NullableType TrackSelection[] createTrackSelections(
+ public @NullableType ExoTrackSelection[] createTrackSelections(
@NullableType Definition[] definitions,
BandwidthMeter bandwidthMeter,
MediaPeriodId mediaPeriodId,
Timeline timeline) {
- @NullableType TrackSelection[] selections = new TrackSelection[definitions.length];
+ @NullableType ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
selections[i] =
definitions[i] == null
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 7bb6a83add..650e055f0b 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
@@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -34,9 +34,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*/
public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
- /**
- * The {@link MediaPeriod} wrapped by this clipping media period.
- */
+ /** The {@link MediaPeriod} wrapped by this clipping media period. */
public final MediaPeriod mediaPeriod;
@Nullable private MediaPeriod.Callback callback;
@@ -98,7 +96,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -250,7 +248,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
private static boolean shouldKeepInitialDiscontinuity(
- long startUs, @NullableType TrackSelection[] selections) {
+ long startUs, @NullableType ExoTrackSelection[] selections) {
// If the clipping start position is non-zero, the clipping sample streams will adjust
// timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
// timestamps can be negative, because sample streams provide buffers starting at a key-frame,
@@ -261,7 +259,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
// However, for tracks where all samples are sync samples, we assume they have random access
// seek behaviour and do not need an initial discontinuity to reset the renderer.
if (startUs != 0) {
- for (TrackSelection trackSelection : selections) {
+ for (ExoTrackSelection trackSelection : selections) {
if (trackSelection != null) {
Format selectedFormat = trackSelection.getSelectedFormat();
if (!MimeTypes.allSamplesAreSyncSamples(
@@ -274,9 +272,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
return false;
}
- /**
- * Wraps a {@link SampleStream} and clips its samples.
- */
+ /** Wraps a {@link SampleStream} and clips its samples. */
private final class ClippingSampleStream implements SampleStream {
public final SampleStream childStream;
@@ -302,8 +298,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
- public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
- boolean requireFormat) {
+ public int readData(
+ FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
if (isPendingInitialDiscontinuity()) {
return C.RESULT_NOTHING_READ;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
index bb82a0c245..31aad16b02 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
@@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.offline.StreamKey;
@@ -100,15 +101,12 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
private static final String TAG = "DefaultMediaSourceFactory";
- private final MediaSourceDrmHelper mediaSourceDrmHelper;
private final DataSource.Factory dataSourceFactory;
private final SparseArray mediaSourceFactories;
@C.ContentType private final int[] supportedTypes;
@Nullable private AdsLoaderProvider adsLoaderProvider;
@Nullable private AdViewProvider adViewProvider;
- @Nullable private DrmSessionManager drmSessionManager;
- @Nullable private List streamKeys;
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long liveTargetOffsetMs;
private long liveMinOffsetMs;
@@ -157,7 +155,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
public DefaultMediaSourceFactory(
DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this.dataSourceFactory = dataSourceFactory;
- mediaSourceDrmHelper = new MediaSourceDrmHelper();
mediaSourceFactories = loadDelegates(dataSourceFactory, extractorsFactory);
supportedTypes = new int[mediaSourceFactories.size()];
for (int i = 0; i < mediaSourceFactories.size(); i++) {
@@ -254,23 +251,41 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return this;
}
+ @SuppressWarnings("deprecation") // Calling through to the same deprecated method.
@Override
public DefaultMediaSourceFactory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
- mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ for (int i = 0; i < mediaSourceFactories.size(); i++) {
+ mediaSourceFactories.valueAt(i).setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ }
return this;
}
+ @SuppressWarnings("deprecation") // Calling through to the same deprecated method.
@Override
public DefaultMediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
- mediaSourceDrmHelper.setDrmUserAgent(userAgent);
+ for (int i = 0; i < mediaSourceFactories.size(); i++) {
+ mediaSourceFactories.valueAt(i).setDrmUserAgent(userAgent);
+ }
return this;
}
+ @SuppressWarnings("deprecation") // Calling through to the same deprecated method.
@Override
public DefaultMediaSourceFactory setDrmSessionManager(
@Nullable DrmSessionManager drmSessionManager) {
- this.drmSessionManager = drmSessionManager;
+ for (int i = 0; i < mediaSourceFactories.size(); i++) {
+ mediaSourceFactories.valueAt(i).setDrmSessionManager(drmSessionManager);
+ }
+ return this;
+ }
+
+ @Override
+ public DefaultMediaSourceFactory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ for (int i = 0; i < mediaSourceFactories.size(); i++) {
+ mediaSourceFactories.valueAt(i).setDrmSessionManagerProvider(drmSessionManagerProvider);
+ }
return this;
}
@@ -278,6 +293,9 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
public DefaultMediaSourceFactory setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ for (int i = 0; i < mediaSourceFactories.size(); i++) {
+ mediaSourceFactories.valueAt(i).setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
+ }
return this;
}
@@ -285,11 +303,13 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
* @deprecated Use {@link MediaItem.Builder#setStreamKeys(List)} and {@link
* #createMediaSource(MediaItem)} instead.
*/
- @SuppressWarnings("deprecation")
+ @SuppressWarnings("deprecation") // Calling through to the same deprecated method.
@Deprecated
@Override
public DefaultMediaSourceFactory setStreamKeys(@Nullable List streamKeys) {
- this.streamKeys = streamKeys != null && !streamKeys.isEmpty() ? streamKeys : null;
+ for (int i = 0; i < mediaSourceFactories.size(); i++) {
+ mediaSourceFactories.valueAt(i).setStreamKeys(streamKeys);
+ }
return this;
}
@@ -298,7 +318,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return Arrays.copyOf(supportedTypes, supportedTypes.length);
}
- @SuppressWarnings("deprecation")
@Override
public MediaSource createMediaSource(MediaItem mediaItem) {
Assertions.checkNotNull(mediaItem.playbackProperties);
@@ -309,13 +328,6 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type);
Assertions.checkNotNull(
mediaSourceFactory, "No suitable media source factory found for content type: " + type);
- mediaSourceFactory.setDrmSessionManager(
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem));
- mediaSourceFactory.setStreamKeys(
- !mediaItem.playbackProperties.streamKeys.isEmpty()
- ? mediaItem.playbackProperties.streamKeys
- : streamKeys);
- mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
// Make sure to retain the very same media item instance, if no value needs to be overridden.
if ((mediaItem.liveConfiguration.targetOffsetMs == C.TIME_UNSET
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 30f85d2dc4..c2fa35275c 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
@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
@@ -154,6 +155,18 @@ public final class ExtractorMediaSource extends CompositeMediaSource {
return this;
}
+ /**
+ * @deprecated Use {@link
+ * ProgressiveMediaSource.Factory#setDrmSessionManagerProvider(DrmSessionManagerProvider)}
+ * instead.
+ */
+ @Deprecated
+ @Override
+ public Factory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ throw new UnsupportedOperationException();
+ }
+
/** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmSessionManager} instead. */
@Deprecated
@Override
@@ -324,7 +337,7 @@ public final class ExtractorMediaSource extends CompositeMediaSource {
.build(),
dataSourceFactory,
extractorsFactory,
- DrmSessionManager.getDummyDrmSessionManager(),
+ DrmSessionManager.DRM_UNSUPPORTED,
loadableLoadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java
index a69835532f..2151119abf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java
@@ -23,7 +23,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@@ -173,7 +173,7 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index 39b207e264..bcbf95a431 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -21,7 +21,7 @@ import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -29,21 +29,23 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All
- * methods are called on the player's internal playback thread, as described in the
- * {@link ExoPlayer} Javadoc.
+ * methods are called on the player's internal playback thread, as described in the {@link
+ * ExoPlayer} Javadoc.
+ *
+ * A {@link MediaPeriod} may only able to provide one {@link SampleStream} corresponding to a
+ * group at any given time, however this {@link SampleStream} may adapt between multiple tracks
+ * within the group.
*/
public interface MediaPeriod extends SequenceableLoader {
- /**
- * A callback to be notified of {@link MediaPeriod} events.
- */
+ /** A callback to be notified of {@link MediaPeriod} events. */
interface Callback extends SequenceableLoader.Callback {
/**
* Called when preparation completes.
*
* Called on the playback thread. After invoking this method, the {@link MediaPeriod} can
- * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],
+ * expect for {@link #selectTracks(ExoTrackSelection[], boolean[], SampleStream[], boolean[],
* long)} to be called with the initial track selection.
*
* @param mediaPeriod The prepared {@link MediaPeriod}.
@@ -88,17 +90,17 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period
- * to load only the parts needed to play the provided {@link TrackSelection TrackSelections}.
+ * to load only the parts needed to play the provided {@link ExoTrackSelection TrackSelections}.
*
*
This method is only called after the period has been prepared.
*
- * @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for
+ * @param trackSelections The {@link ExoTrackSelection TrackSelections} describing the tracks for
* which stream keys are requested.
* @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty
* list if filtering is not possible and the entire media needs to be loaded to play the
* selected tracks.
*/
- default List getStreamKeys(List trackSelections) {
+ default List getStreamKeys(List trackSelections) {
return Collections.emptyList();
}
@@ -113,8 +115,8 @@ public interface MediaPeriod extends SequenceableLoader {
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created.
*
- * Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and
- * any references to them must be updated to point to the new selections.
+ *
Note that previously passed {@link ExoTrackSelection TrackSelections} are no longer valid,
+ * and any references to them must be updated to point to the new selections.
*
*
This method is only called after the period has been prepared.
*
@@ -133,7 +135,7 @@ public interface MediaPeriod extends SequenceableLoader {
* @return The actual position at which the tracks were enabled, in microseconds.
*/
long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java
index 204220e334..7242c2a214 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java
@@ -20,7 +20,9 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
@@ -56,41 +58,80 @@ public interface MediaSourceFactory {
return this;
}
+ /**
+ * Sets the {@link DrmSessionManagerProvider} used to obtain a {@link DrmSessionManager} for a
+ * {@link MediaItem}.
+ *
+ *
If not set, {@link DefaultDrmSessionManagerProvider} is used.
+ *
+ *
If set, calls to the following (deprecated) methods are ignored:
+ *
+ *
+ * - {@link #setDrmUserAgent(String)}
+ *
- {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory)}
+ *
+ *
+ * @return This factory, for convenience.
+ */
+ MediaSourceFactory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider);
+
/**
* Sets the {@link DrmSessionManager} to use for all media items regardless of their {@link
* MediaItem.DrmConfiguration}.
*
+ * Calling this with a non-null {@code drmSessionManager} is equivalent to calling {@code
+ * setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)}.
+ *
* @param drmSessionManager The {@link DrmSessionManager}, or {@code null} to use the {@link
* DefaultDrmSessionManager}.
* @return This factory, for convenience.
+ * @deprecated Use {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} and pass an
+ * implementation that always returns the same instance.
*/
+ @Deprecated
MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager);
/**
* Sets the {@link HttpDataSource.Factory} to be used for creating {@link HttpMediaDrmCallback
* HttpMediaDrmCallbacks} to execute key and provisioning requests over HTTP.
*
- *
In case a {@link DrmSessionManager} has been set by {@link
- * #setDrmSessionManager(DrmSessionManager)}, this data source factory is ignored.
+ *
Calls to this method are ignored if either a {@link
+ * #setDrmSessionManagerProvider(DrmSessionManagerProvider) DrmSessionManager provider} or {@link
+ * #setDrmSessionManager(DrmSessionManager) concrete DrmSessionManager} are provided.
*
* @param drmHttpDataSourceFactory The HTTP data source factory, or {@code null} to use {@link
* DefaultHttpDataSourceFactory}.
* @return This factory, for convenience.
+ * @deprecated Use {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} and pass an
+ * implementation that configures the returned {@link DrmSessionManager} with the desired
+ * {@link HttpDataSource.Factory}.
*/
+ @Deprecated
MediaSourceFactory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory);
/**
* Sets the optional user agent to be used for DRM requests.
*
- *
In case a factory has been set by {@link
- * #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} or a {@link DrmSessionManager} has been
- * set by {@link #setDrmSessionManager(DrmSessionManager)}, this user agent is ignored.
+ *
Calls to this method are ignored if any of the following are provided:
+ *
+ *
+ * - A {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider) DrmSessionManager
+ * provider}.
+ *
- A {@link #setDrmSessionManager(DrmSessionManager) concrete DrmSessionManager}.
+ *
- A {@link #setDrmHttpDataSourceFactory(HttpDataSource.Factory) DRM
+ * HttpDataSource.Factory}.
+ *
*
* @param userAgent The user agent to be used for DRM requests, or {@code null} to use the
* default.
* @return This factory, for convenience.
+ * @deprecated Use {@link #setDrmSessionManagerProvider(DrmSessionManagerProvider)} and pass an
+ * implementation that configures the returned {@link DrmSessionManager} with the desired
+ * {@code userAgent}.
*/
+ @Deprecated
MediaSourceFactory setDrmUserAgent(@Nullable String userAgent);
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
index 0dae1ad6f9..860d9a3b95 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -23,7 +23,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.offline.StreamKey;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
@@ -33,9 +33,7 @@ import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-/**
- * Merges multiple {@link MediaPeriod}s.
- */
+/** Merges multiple {@link MediaPeriod}s. */
/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
private final MediaPeriod[] periods;
@@ -100,7 +98,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -126,15 +124,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Select tracks for each child, copying the resulting streams back into a new streams array.
@NullableType SampleStream[] newStreams = new SampleStream[selections.length];
@NullableType SampleStream[] childStreams = new SampleStream[selections.length];
- @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];
+ @NullableType ExoTrackSelection[] childSelections = new ExoTrackSelection[selections.length];
ArrayList enabledPeriodsList = new ArrayList<>(periods.length);
for (int i = 0; i < periods.length; i++) {
for (int j = 0; j < selections.length; j++) {
childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
}
- long selectPositionUs = periods[i].selectTracks(childSelections, mayRetainStreamFlags,
- childStreams, streamResetFlags, positionUs);
+ long selectPositionUs =
+ periods[i].selectTracks(
+ childSelections, mayRetainStreamFlags, childStreams, streamResetFlags, positionUs);
if (i == 0) {
positionUs = selectPositionUs;
} else if (selectPositionUs != positionUs) {
@@ -314,13 +313,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
- public List getStreamKeys(List trackSelections) {
+ public List getStreamKeys(List trackSelections) {
return mediaPeriod.getStreamKeys(trackSelections);
}
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
index 5d7636be6a..f7b88fcab8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
@@ -40,7 +40,7 @@ import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -252,7 +252,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -277,7 +277,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Select new tracks.
for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) {
- TrackSelection selection = selections[i];
+ ExoTrackSelection selection = selections[i];
Assertions.checkState(selection.length() == 1);
Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
int track = tracks.indexOf(selection.getTrackGroup());
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
index e18028571f..fe249df6ff 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
@@ -22,7 +22,9 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
@@ -51,10 +53,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource
public static final class Factory implements MediaSourceFactory {
private final DataSource.Factory dataSourceFactory;
- private final MediaSourceDrmHelper mediaSourceDrmHelper;
private ExtractorsFactory extractorsFactory;
- @Nullable private DrmSessionManager drmSessionManager;
+ private boolean usingCustomDrmSessionManagerProvider;
+ private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private int continueLoadingCheckIntervalBytes;
@Nullable private String customCacheKey;
@@ -79,7 +81,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) {
this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory;
- mediaSourceDrmHelper = new MediaSourceDrmHelper();
+ drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES;
}
@@ -148,21 +150,42 @@ public final class ProgressiveMediaSource extends BaseMediaSource
}
@Override
+ public Factory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ if (drmSessionManagerProvider != null) {
+ this.drmSessionManagerProvider = drmSessionManagerProvider;
+ this.usingCustomDrmSessionManagerProvider = true;
+ } else {
+ this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
+ this.usingCustomDrmSessionManagerProvider = false;
+ }
+ return this;
+ }
+
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
- this.drmSessionManager = drmSessionManager;
+ if (drmSessionManager == null) {
+ setDrmSessionManagerProvider(null);
+ } else {
+ setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
+ }
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
- mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider)
+ .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ }
return this;
}
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
- mediaSourceDrmHelper.setDrmUserAgent(userAgent);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent);
+ }
return this;
}
@@ -198,7 +221,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
mediaItem,
dataSourceFactory,
extractorsFactory,
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
+ drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
continueLoadingCheckIntervalBytes);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java
index 797b5ad30b..5bc1482e68 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java
@@ -115,39 +115,25 @@ import java.util.Arrays;
}
/**
- * Reads data from the rolling buffer to populate a decoder input buffer.
+ * Reads data from the rolling buffer to populate a decoder input buffer, and advances the read
+ * position.
*
* @param buffer The buffer to populate.
* @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
*/
public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
- // Read encryption data if the sample is encrypted.
- if (buffer.isEncrypted()) {
- readEncryptionData(buffer, extrasHolder);
- }
- // Read sample data, extracting supplemental data into a separate buffer if needed.
- if (buffer.hasSupplementalData()) {
- // If there is supplemental data, the sample data is prefixed by its size.
- scratch.reset(4);
- readData(extrasHolder.offset, scratch.getData(), 4);
- int sampleSize = scratch.readUnsignedIntToInt();
- extrasHolder.offset += 4;
- extrasHolder.size -= 4;
+ readAllocationNode = readSampleData(readAllocationNode, buffer, extrasHolder, scratch);
+ }
- // Write the sample data.
- buffer.ensureSpaceForWrite(sampleSize);
- readData(extrasHolder.offset, buffer.data, sampleSize);
- extrasHolder.offset += sampleSize;
- extrasHolder.size -= sampleSize;
-
- // Write the remaining data as supplemental data.
- buffer.resetSupplementalData(extrasHolder.size);
- readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size);
- } else {
- // Write the sample data.
- buffer.ensureSpaceForWrite(extrasHolder.size);
- readData(extrasHolder.offset, buffer.data, extrasHolder.size);
- }
+ /**
+ * Peeks data from the rolling buffer to populate a decoder input buffer, without advancing the
+ * read position.
+ *
+ * @param buffer The buffer to populate.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ */
+ public void peekToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
+ readSampleData(readAllocationNode, buffer, extrasHolder, scratch);
}
/**
@@ -210,151 +196,6 @@ import java.util.Arrays;
// Private methods.
- /**
- * Reads encryption data for the current sample.
- *
- * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link
- * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same
- * value is added to {@link SampleExtrasHolder#offset}.
- *
- * @param buffer The buffer into which the encryption data should be written.
- * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
- */
- private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) {
- long offset = extrasHolder.offset;
-
- // Read the signal byte.
- scratch.reset(1);
- readData(offset, scratch.getData(), 1);
- offset++;
- byte signalByte = scratch.getData()[0];
- boolean subsampleEncryption = (signalByte & 0x80) != 0;
- int ivSize = signalByte & 0x7F;
-
- // Read the initialization vector.
- CryptoInfo cryptoInfo = buffer.cryptoInfo;
- if (cryptoInfo.iv == null) {
- cryptoInfo.iv = new byte[16];
- } else {
- // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0.
- Arrays.fill(cryptoInfo.iv, (byte) 0);
- }
- readData(offset, cryptoInfo.iv, ivSize);
- offset += ivSize;
-
- // Read the subsample count, if present.
- int subsampleCount;
- if (subsampleEncryption) {
- scratch.reset(2);
- readData(offset, scratch.getData(), 2);
- offset += 2;
- subsampleCount = scratch.readUnsignedShort();
- } else {
- subsampleCount = 1;
- }
-
- // Write the clear and encrypted subsample sizes.
- @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
- if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
- clearDataSizes = new int[subsampleCount];
- }
- @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
- if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
- encryptedDataSizes = new int[subsampleCount];
- }
- if (subsampleEncryption) {
- int subsampleDataLength = 6 * subsampleCount;
- scratch.reset(subsampleDataLength);
- readData(offset, scratch.getData(), subsampleDataLength);
- offset += subsampleDataLength;
- scratch.setPosition(0);
- for (int i = 0; i < subsampleCount; i++) {
- clearDataSizes[i] = scratch.readUnsignedShort();
- encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
- }
- } else {
- clearDataSizes[0] = 0;
- encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
- }
-
- // Populate the cryptoInfo.
- CryptoData cryptoData = Util.castNonNull(extrasHolder.cryptoData);
- cryptoInfo.set(
- subsampleCount,
- clearDataSizes,
- encryptedDataSizes,
- cryptoData.encryptionKey,
- cryptoInfo.iv,
- cryptoData.cryptoMode,
- cryptoData.encryptedBlocks,
- cryptoData.clearBlocks);
-
- // Adjust the offset and size to take into account the bytes read.
- int bytesRead = (int) (offset - extrasHolder.offset);
- extrasHolder.offset += bytesRead;
- extrasHolder.size -= bytesRead;
- }
-
- /**
- * Reads data from the front of the rolling buffer.
- *
- * @param absolutePosition The absolute position from which data should be read.
- * @param target The buffer into which data should be written.
- * @param length The number of bytes to read.
- */
- private void readData(long absolutePosition, ByteBuffer target, int length) {
- advanceReadTo(absolutePosition);
- int remaining = length;
- while (remaining > 0) {
- int toCopy = min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
- Allocation allocation = readAllocationNode.allocation;
- target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy);
- remaining -= toCopy;
- absolutePosition += toCopy;
- if (absolutePosition == readAllocationNode.endPosition) {
- readAllocationNode = readAllocationNode.next;
- }
- }
- }
-
- /**
- * Reads data from the front of the rolling buffer.
- *
- * @param absolutePosition The absolute position from which data should be read.
- * @param target The array into which data should be written.
- * @param length The number of bytes to read.
- */
- private void readData(long absolutePosition, byte[] target, int length) {
- advanceReadTo(absolutePosition);
- int remaining = length;
- while (remaining > 0) {
- int toCopy = min(remaining, (int) (readAllocationNode.endPosition - absolutePosition));
- Allocation allocation = readAllocationNode.allocation;
- System.arraycopy(
- allocation.data,
- readAllocationNode.translateOffset(absolutePosition),
- target,
- length - remaining,
- toCopy);
- remaining -= toCopy;
- absolutePosition += toCopy;
- if (absolutePosition == readAllocationNode.endPosition) {
- readAllocationNode = readAllocationNode.next;
- }
- }
- }
-
- /**
- * Advances the read position to the specified absolute position.
- *
- * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced.
- */
- private void advanceReadTo(long absolutePosition) {
- while (absolutePosition >= readAllocationNode.endPosition) {
- readAllocationNode = readAllocationNode.next;
- }
- }
-
/**
* Clears allocation nodes starting from {@code fromNode}.
*
@@ -409,6 +250,214 @@ import java.util.Arrays;
}
}
+ /**
+ * Reads data from the rolling buffer to populate a decoder input buffer.
+ *
+ * @param allocationNode The first {@link AllocationNode} containing data yet to be read.
+ * @param buffer The buffer to populate.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ * @param scratch A scratch {@link ParsableByteArray}.
+ * @return The first {@link AllocationNode} that contains unread bytes after the last byte that
+ * the invocation read.
+ */
+ private static AllocationNode readSampleData(
+ AllocationNode allocationNode,
+ DecoderInputBuffer buffer,
+ SampleExtrasHolder extrasHolder,
+ ParsableByteArray scratch) {
+ if (buffer.isEncrypted()) {
+ allocationNode = readEncryptionData(allocationNode, buffer, extrasHolder, scratch);
+ }
+ // Read sample data, extracting supplemental data into a separate buffer if needed.
+ if (buffer.hasSupplementalData()) {
+ // If there is supplemental data, the sample data is prefixed by its size.
+ scratch.reset(4);
+ allocationNode = readData(allocationNode, extrasHolder.offset, scratch.getData(), 4);
+ int sampleSize = scratch.readUnsignedIntToInt();
+ extrasHolder.offset += 4;
+ extrasHolder.size -= 4;
+
+ // Write the sample data.
+ buffer.ensureSpaceForWrite(sampleSize);
+ allocationNode = readData(allocationNode, extrasHolder.offset, buffer.data, sampleSize);
+ extrasHolder.offset += sampleSize;
+ extrasHolder.size -= sampleSize;
+
+ // Write the remaining data as supplemental data.
+ buffer.resetSupplementalData(extrasHolder.size);
+ allocationNode =
+ readData(allocationNode, extrasHolder.offset, buffer.supplementalData, extrasHolder.size);
+ } else {
+ // Write the sample data.
+ buffer.ensureSpaceForWrite(extrasHolder.size);
+ allocationNode =
+ readData(allocationNode, extrasHolder.offset, buffer.data, extrasHolder.size);
+ }
+ return allocationNode;
+ }
+
+ /**
+ * Reads encryption data for the sample described by {@code extrasHolder}.
+ *
+ *
The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link
+ * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same
+ * value is added to {@link SampleExtrasHolder#offset}.
+ *
+ * @param allocationNode The first {@link AllocationNode} containing data yet to be read.
+ * @param buffer The buffer into which the encryption data should be written.
+ * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+ * @param scratch A scratch {@link ParsableByteArray}.
+ * @return The first {@link AllocationNode} that contains unread bytes after this method returns.
+ */
+ private static AllocationNode readEncryptionData(
+ AllocationNode allocationNode,
+ DecoderInputBuffer buffer,
+ SampleExtrasHolder extrasHolder,
+ ParsableByteArray scratch) {
+ long offset = extrasHolder.offset;
+
+ // Read the signal byte.
+ scratch.reset(1);
+ allocationNode = readData(allocationNode, offset, scratch.getData(), 1);
+ offset++;
+ byte signalByte = scratch.getData()[0];
+ boolean subsampleEncryption = (signalByte & 0x80) != 0;
+ int ivSize = signalByte & 0x7F;
+
+ // Read the initialization vector.
+ CryptoInfo cryptoInfo = buffer.cryptoInfo;
+ if (cryptoInfo.iv == null) {
+ cryptoInfo.iv = new byte[16];
+ } else {
+ // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0.
+ Arrays.fill(cryptoInfo.iv, (byte) 0);
+ }
+ allocationNode = readData(allocationNode, offset, cryptoInfo.iv, ivSize);
+ offset += ivSize;
+
+ // Read the subsample count, if present.
+ int subsampleCount;
+ if (subsampleEncryption) {
+ scratch.reset(2);
+ allocationNode = readData(allocationNode, offset, scratch.getData(), 2);
+ offset += 2;
+ subsampleCount = scratch.readUnsignedShort();
+ } else {
+ subsampleCount = 1;
+ }
+
+ // Write the clear and encrypted subsample sizes.
+ @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData;
+ if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+ clearDataSizes = new int[subsampleCount];
+ }
+ @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData;
+ if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+ encryptedDataSizes = new int[subsampleCount];
+ }
+ if (subsampleEncryption) {
+ int subsampleDataLength = 6 * subsampleCount;
+ scratch.reset(subsampleDataLength);
+ allocationNode = readData(allocationNode, offset, scratch.getData(), subsampleDataLength);
+ offset += subsampleDataLength;
+ scratch.setPosition(0);
+ for (int i = 0; i < subsampleCount; i++) {
+ clearDataSizes[i] = scratch.readUnsignedShort();
+ encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
+ }
+ } else {
+ clearDataSizes[0] = 0;
+ encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
+ }
+
+ // Populate the cryptoInfo.
+ CryptoData cryptoData = Util.castNonNull(extrasHolder.cryptoData);
+ cryptoInfo.set(
+ subsampleCount,
+ clearDataSizes,
+ encryptedDataSizes,
+ cryptoData.encryptionKey,
+ cryptoInfo.iv,
+ cryptoData.cryptoMode,
+ cryptoData.encryptedBlocks,
+ cryptoData.clearBlocks);
+
+ // Adjust the offset and size to take into account the bytes read.
+ int bytesRead = (int) (offset - extrasHolder.offset);
+ extrasHolder.offset += bytesRead;
+ extrasHolder.size -= bytesRead;
+ return allocationNode;
+ }
+
+ /**
+ * Reads data from {@code allocationNode} and its following nodes.
+ *
+ * @param allocationNode The first {@link AllocationNode} containing data yet to be read.
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The buffer into which data should be written.
+ * @param length The number of bytes to read.
+ * @return The first {@link AllocationNode} that contains unread bytes after this method returns.
+ */
+ private static AllocationNode readData(
+ AllocationNode allocationNode, long absolutePosition, ByteBuffer target, int length) {
+ allocationNode = getNodeContainingPosition(allocationNode, absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = min(remaining, (int) (allocationNode.endPosition - absolutePosition));
+ Allocation allocation = allocationNode.allocation;
+ target.put(allocation.data, allocationNode.translateOffset(absolutePosition), toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == allocationNode.endPosition) {
+ allocationNode = allocationNode.next;
+ }
+ }
+ return allocationNode;
+ }
+
+ /**
+ * Reads data from {@code allocationNode} and its following nodes.
+ *
+ * @param allocationNode The first {@link AllocationNode} containing data yet to be read.
+ * @param absolutePosition The absolute position from which data should be read.
+ * @param target The array into which data should be written.
+ * @param length The number of bytes to read.
+ * @return The first {@link AllocationNode} that contains unread bytes after this method returns.
+ */
+ private static AllocationNode readData(
+ AllocationNode allocationNode, long absolutePosition, byte[] target, int length) {
+ allocationNode = getNodeContainingPosition(allocationNode, absolutePosition);
+ int remaining = length;
+ while (remaining > 0) {
+ int toCopy = min(remaining, (int) (allocationNode.endPosition - absolutePosition));
+ Allocation allocation = allocationNode.allocation;
+ System.arraycopy(
+ allocation.data,
+ allocationNode.translateOffset(absolutePosition),
+ target,
+ length - remaining,
+ toCopy);
+ remaining -= toCopy;
+ absolutePosition += toCopy;
+ if (absolutePosition == allocationNode.endPosition) {
+ allocationNode = allocationNode.next;
+ }
+ }
+ return allocationNode;
+ }
+
+ /**
+ * Returns the {@link AllocationNode} in {@code allocationNode}'s chain which contains the given
+ * {@code absolutePosition}.
+ */
+ private static AllocationNode getNodeContainingPosition(
+ AllocationNode allocationNode, long absolutePosition) {
+ while (absolutePosition >= allocationNode.endPosition) {
+ allocationNode = allocationNode.next;
+ }
+ return allocationNode;
+ }
+
/** A node in a linked list of {@link Allocation}s held by the output. */
private static final class AllocationNode {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
index ccbfd6a0b0..77e17c84b1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
@@ -377,6 +377,20 @@ public class SampleQueue implements TrackOutput {
return mayReadSample(relativeReadIndex);
}
+ /** Equivalent to {@link #read}, except it never advances the read position. */
+ public final int peek(
+ FormatHolder formatHolder,
+ DecoderInputBuffer buffer,
+ boolean formatRequired,
+ boolean loadingFinished) {
+ int result =
+ peekSampleMetadata(formatHolder, buffer, formatRequired, loadingFinished, extrasHolder);
+ if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) {
+ sampleDataQueue.peekToBuffer(buffer, extrasHolder);
+ }
+ return result;
+ }
+
/**
* Attempts to read from the queue.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java
index 15861a1922..b802717ee2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java
@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
@@ -194,7 +194,7 @@ public final class SilenceMediaSource extends BaseMediaSource {
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
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 a24dedea03..9c9f2265ad 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
@@ -31,10 +31,7 @@ public final class SinglePeriodTimeline extends Timeline {
private static final Object UID = new Object();
private static final MediaItem MEDIA_ITEM =
- new MediaItem.Builder()
- .setMediaId("com.google.android.exoplayer2.source.SinglePeriodTimeline")
- .setUri(Uri.EMPTY)
- .build();
+ new MediaItem.Builder().setMediaId("SinglePeriodTimeline").setUri(Uri.EMPTY).build();
private final long presentationStartTimeMs;
private final long windowStartTimeMs;
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 352785d37d..9e5d8aae54 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
@@ -22,7 +22,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.upstream.StatsDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
@@ -41,15 +42,13 @@ import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-/**
- * A {@link MediaPeriod} with a single sample.
- */
-/* package */ final class SingleSampleMediaPeriod implements MediaPeriod,
- Loader.Callback {
+/** A {@link MediaPeriod} with a single sample. */
+/* package */ final class SingleSampleMediaPeriod
+ implements MediaPeriod, Loader.Callback {
- /**
- * The initial size of the allocation used to hold the sample data.
- */
+ private static final String TAG = "SingleSampleMediaPeriod";
+
+ /** The initial size of the allocation used to hold the sample data. */
private static final int INITIAL_SAMPLE_SIZE = 1024;
private final DataSpec dataSpec;
@@ -113,7 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -294,6 +293,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
LoadErrorAction action;
if (treatLoadErrorsAsEndOfStream && errorCanBePropagated) {
+ Log.w(TAG, "Loading failed, treating as end-of-stream.", error);
loadingFinished = true;
action = Loader.DONT_RETRY;
} else {
@@ -348,8 +348,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
- public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
- boolean requireFormat) {
+ public int readData(
+ FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
maybeNotifyDownstreamFormat();
if (streamState == STREAM_STATE_END_OF_STREAM) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
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 56dd4ebef2..dd25d74e10 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
@@ -145,6 +145,7 @@ public final class Cea708Decoder extends CeaDecoder {
private final ParsableByteArray ccData;
private final ParsableBitArray serviceBlockPacket;
+ private int previousSequenceNumber;
// TODO: Use isWideAspectRatio in decoding.
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final boolean isWideAspectRatio;
@@ -162,6 +163,7 @@ public final class Cea708Decoder extends CeaDecoder {
public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) {
ccData = new ParsableByteArray();
serviceBlockPacket = new ParsableBitArray();
+ previousSequenceNumber = C.INDEX_UNSET;
selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel;
isWideAspectRatio =
initializationData != null
@@ -231,6 +233,18 @@ public final class Cea708Decoder extends CeaDecoder {
finalizeCurrentPacket();
int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
+ if (previousSequenceNumber != C.INDEX_UNSET
+ && sequenceNumber != (previousSequenceNumber + 1) % 4) {
+ resetCueBuilders();
+ Log.w(
+ TAG,
+ "Sequence number discontinuity. previous="
+ + previousSequenceNumber
+ + " current="
+ + sequenceNumber);
+ }
+ previousSequenceNumber = sequenceNumber;
+
int packetSize = ccData1 & 0x3F; // last 6 bits
if (packetSize == 0) {
packetSize = 64;
@@ -270,10 +284,18 @@ public final class Cea708Decoder extends CeaDecoder {
@RequiresNonNull("currentDtvCcPacket")
private void processCurrentPacket() {
if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {
- Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1)
- + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number "
- + currentDtvCcPacket.sequenceNumber + "); ignoring packet");
- return;
+ Log.d(
+ TAG,
+ "DtvCcPacket ended prematurely; size is "
+ + (currentDtvCcPacket.packetSize * 2 - 1)
+ + ", but current index is "
+ + currentDtvCcPacket.currentIndex
+ + " (sequence number "
+ + currentDtvCcPacket.sequenceNumber
+ + ");");
+ // We've received cc_type=0x03 (packet start) before receiving packetSize byte pairs of data.
+ // This might indicate a byte pair has been lost, but we'll still attempt to process the data
+ // we have received.
}
serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);
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 f44db4924f..b8e047dbcb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java
@@ -19,6 +19,8 @@ import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_FRACTION;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.text.Layout;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
@@ -301,7 +303,18 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
SsaStyle.Overrides styleOverrides,
float screenWidth,
float screenHeight) {
- Cue.Builder cue = new Cue.Builder().setText(text);
+ SpannableString spannableText = new SpannableString(text);
+ Cue.Builder cue = new Cue.Builder().setText(spannableText);
+
+ if (style != null) {
+ if (style.primaryColor != null) {
+ spannableText.setSpan(
+ new ForegroundColorSpan(style.primaryColor),
+ /* start= */ 0,
+ /* end= */ spannableText.length(),
+ SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
@SsaStyle.SsaAlignment int alignment;
if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java
index 0cba339034..bd378cccec 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java
@@ -17,16 +17,20 @@
package com.google.android.exoplayer2.text.ssa;
import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX;
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.graphics.Color;
import android.graphics.PointF;
import android.text.TextUtils;
+import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.util.regex.Matcher;
@@ -85,15 +89,18 @@ import java.util.regex.Pattern;
public final String name;
@SsaAlignment public final int alignment;
+ @Nullable @ColorInt public final Integer primaryColor;
- private SsaStyle(String name, @SsaAlignment int alignment) {
+ private SsaStyle(
+ String name, @SsaAlignment int alignment, @Nullable @ColorInt Integer primaryColor) {
this.name = name;
this.alignment = alignment;
+ this.primaryColor = primaryColor;
}
@Nullable
public static SsaStyle fromStyleLine(String styleLine, Format format) {
- Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
+ checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX));
String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ",");
if (styleValues.length != format.length) {
Log.w(
@@ -105,7 +112,9 @@ import java.util.regex.Pattern;
}
try {
return new SsaStyle(
- styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex]));
+ styleValues[format.nameIndex].trim(),
+ parseAlignment(styleValues[format.alignmentIndex].trim()),
+ parseColor(styleValues[format.primaryColorIndex].trim()));
} catch (RuntimeException e) {
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
return null;
@@ -144,6 +153,44 @@ import java.util.regex.Pattern;
}
}
+ /**
+ * Parses a SSA V4+ color expression.
+ *
+ * A SSA V4+ color can be represented in hex {@code ("&HAABBGGRR")} or in 64-bit decimal format
+ * (byte order AABBGGRR). In both cases the alpha channel's value needs to be inverted because in
+ * SSA the 0xFF alpha value means transparent and 0x00 means opaque which is the opposite from the
+ * Android {@link ColorInt} representation.
+ *
+ * @param ssaColorExpression A SSA V4+ color expression.
+ * @return The parsed color value, or null if parsing failed.
+ */
+ @Nullable
+ @ColorInt
+ public static Integer parseColor(String ssaColorExpression) {
+ // We use a long because the value is an unsigned 32-bit number, so can be larger than
+ // Integer.MAX_VALUE.
+ long abgr;
+ try {
+ abgr =
+ ssaColorExpression.startsWith("&H")
+ // Parse color from hex format (&HAABBGGRR).
+ ? Long.parseLong(ssaColorExpression.substring(2), /* radix= */ 16)
+ // Parse color from decimal format (bytes order AABBGGRR).
+ : Long.parseLong(ssaColorExpression);
+ // Ensure only the bottom 4 bytes of abgr are set.
+ checkArgument(abgr <= 0xFFFFFFFFL);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed to parse color expression: '" + ssaColorExpression + "'", e);
+ return null;
+ }
+ // Convert ABGR to ARGB.
+ int a = Ints.checkedCast(((abgr >> 24) & 0xFF) ^ 0xFF); // Flip alpha.
+ int b = Ints.checkedCast((abgr >> 16) & 0xFF);
+ int g = Ints.checkedCast((abgr >> 8) & 0xFF);
+ int r = Ints.checkedCast(abgr & 0xFF);
+ return Color.argb(a, r, g, b);
+ }
+
/**
* Represents a {@code Format:} line from the {@code [V4+ Styles]} section
*
@@ -154,11 +201,13 @@ import java.util.regex.Pattern;
public final int nameIndex;
public final int alignmentIndex;
+ public final int primaryColorIndex;
public final int length;
- private Format(int nameIndex, int alignmentIndex, int length) {
+ private Format(int nameIndex, int alignmentIndex, int primaryColorIndex, int length) {
this.nameIndex = nameIndex;
this.alignmentIndex = alignmentIndex;
+ this.primaryColorIndex = primaryColorIndex;
this.length = length;
}
@@ -171,6 +220,7 @@ import java.util.regex.Pattern;
public static Format fromFormatLine(String styleFormatLine) {
int nameIndex = C.INDEX_UNSET;
int alignmentIndex = C.INDEX_UNSET;
+ int primaryColorIndex = C.INDEX_UNSET;
String[] keys =
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
for (int i = 0; i < keys.length; i++) {
@@ -181,9 +231,14 @@ import java.util.regex.Pattern;
case "alignment":
alignmentIndex = i;
break;
+ case "primarycolour":
+ primaryColorIndex = i;
+ break;
}
}
- return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null;
+ return nameIndex != C.INDEX_UNSET
+ ? new Format(nameIndex, alignmentIndex, primaryColorIndex, keys.length)
+ : null;
}
}
@@ -237,8 +292,7 @@ import java.util.regex.Pattern;
// Ignore invalid \pos() or \move() function.
}
try {
- @SsaAlignment
- int parsedAlignment = parseAlignmentOverride(braceContents);
+ @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents);
if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) {
alignment = parsedAlignment;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
index 9d5cc78d0b..bd2e18ad92 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -38,13 +38,13 @@ import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
- * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one
- * of highest quality given the current network conditions and the state of the buffer.
+ * A bandwidth based adaptive {@link ExoTrackSelection}, whose selected track is updated to be the
+ * one of highest quality given the current network conditions and the state of the buffer.
*/
public class AdaptiveTrackSelection extends BaseTrackSelection {
/** Factory for {@link AdaptiveTrackSelection} instances. */
- public static class Factory implements TrackSelection.Factory {
+ public static class Factory implements ExoTrackSelection.Factory {
private final int minDurationForQualityIncreaseMs;
private final int maxDurationForQualityDecreaseMs;
@@ -132,14 +132,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
}
@Override
- public final @NullableType TrackSelection[] createTrackSelections(
+ public final @NullableType ExoTrackSelection[] createTrackSelections(
@NullableType Definition[] definitions,
BandwidthMeter bandwidthMeter,
MediaPeriodId mediaPeriodId,
Timeline timeline) {
ImmutableList> adaptationCheckpoints =
getAdaptationCheckpoints(definitions);
- TrackSelection[] selections = new TrackSelection[definitions.length];
+ ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
@Nullable Definition definition = definitions[i];
if (definition == null || definition.tracks.length == 0) {
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 4be4bf7075..17c486b45a 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
@@ -28,27 +28,17 @@ import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.List;
-/**
- * An abstract base class suitable for most {@link TrackSelection} implementations.
- */
-public abstract class BaseTrackSelection implements TrackSelection {
+/** An abstract base class suitable for most {@link ExoTrackSelection} implementations. */
+public abstract class BaseTrackSelection implements ExoTrackSelection {
- /**
- * The selected {@link TrackGroup}.
- */
+ /** The selected {@link TrackGroup}. */
protected final TrackGroup group;
- /**
- * The number of selected tracks within the {@link TrackGroup}. Always greater than zero.
- */
+ /** The number of selected tracks within the {@link TrackGroup}. Always greater than zero. */
protected final int length;
- /**
- * The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth.
- */
+ /** The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth. */
protected final int[] tracks;
- /**
- * The {@link Format}s of the selected tracks, in order of decreasing bandwidth.
- */
+ /** The {@link Format}s of the selected tracks, in order of decreasing bandwidth. */
private final Format[] formats;
/** Selected track exclusion timestamps, in order of decreasing bandwidth. */
private final long[] excludeUntilTimes;
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 05988b4748..627df86cf6 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
@@ -222,7 +222,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
*
* @param context Any context.
*/
-
public ParametersBuilder(Context context) {
super(context);
setInitialValuesWithoutContext();
@@ -826,9 +825,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return this;
}
- /**
- * Builds a {@link Parameters} instance with the selected values.
- */
+ /** Builds a {@link Parameters} instance with the selected values. */
public Parameters build() {
return new Parameters(
// Video
@@ -1614,6 +1611,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* dimension).
*/
private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f;
+
private static final int[] NO_TRACKS = new int[0];
/** Ordering of two format values. A known value is considered greater than Format#NO_VALUE. */
private static final Ordering FORMAT_VALUE_ORDERING =
@@ -1625,7 +1623,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/** Ordering where all elements are equal. */
private static final Ordering NO_ORDER = Ordering.from((first, second) -> 0);
- private final TrackSelection.Factory trackSelectionFactory;
+ private final ExoTrackSelection.Factory trackSelectionFactory;
private final AtomicReference parametersReference;
/** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */
@@ -1634,9 +1632,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
this(Parameters.DEFAULT_WITHOUT_CONTEXT, new AdaptiveTrackSelection.Factory());
}
- /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */
+ /** @deprecated Use {@link #DefaultTrackSelector(Context, ExoTrackSelection.Factory)}. */
@Deprecated
- public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) {
+ public DefaultTrackSelector(ExoTrackSelection.Factory trackSelectionFactory) {
this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory);
}
@@ -1647,17 +1645,18 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/**
* @param context Any {@link Context}.
- * @param trackSelectionFactory A factory for {@link TrackSelection}s.
+ * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s.
*/
- public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) {
+ public DefaultTrackSelector(Context context, ExoTrackSelection.Factory trackSelectionFactory) {
this(Parameters.getDefaults(context), trackSelectionFactory);
}
/**
* @param parameters Initial {@link Parameters}.
- * @param trackSelectionFactory A factory for {@link TrackSelection}s.
+ * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s.
*/
- public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) {
+ public DefaultTrackSelector(
+ Parameters parameters, ExoTrackSelection.Factory trackSelectionFactory) {
this.trackSelectionFactory = trackSelectionFactory;
parametersReference = new AtomicReference<>(parameters);
}
@@ -1700,7 +1699,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// MappingTrackSelector implementation.
@Override
- protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]>
+ protected final Pair<@NullableType RendererConfiguration[], @NullableType ExoTrackSelection[]>
selectTracks(
MappedTrackInfo mappedTrackInfo,
@Capabilities int[][][] rendererFormatSupports,
@@ -1710,7 +1709,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
throws ExoPlaybackException {
Parameters params = parametersReference.get();
int rendererCount = mappedTrackInfo.getRendererCount();
- TrackSelection.@NullableType Definition[] definitions =
+ ExoTrackSelection.@NullableType Definition[] definitions =
selectAllTracks(
mappedTrackInfo,
rendererFormatSupports,
@@ -1729,7 +1728,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
definitions[i] =
override == null
? null
- : new TrackSelection.Definition(
+ : new ExoTrackSelection.Definition(
rendererTrackGroups.get(override.groupIndex),
override.tracks,
override.reason,
@@ -1738,14 +1737,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@NullableType
- TrackSelection[] rendererTrackSelections =
+ ExoTrackSelection[] rendererTrackSelections =
trackSelectionFactory.createTrackSelections(
definitions, getBandwidthMeter(), mediaPeriodId, timeline);
// Initialize the renderer configurations to the default configuration for all renderers with
// selections, and null otherwise.
- @NullableType RendererConfiguration[] rendererConfigurations =
- new RendererConfiguration[rendererCount];
+ @NullableType
+ RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount];
for (int i = 0; i < rendererCount; i++) {
boolean forceRendererDisabled = params.getRendererDisabled(i);
boolean rendererEnabled =
@@ -1779,19 +1778,19 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* renderer, track group and track (in that order).
* @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type
* adaptation for the renderer.
- * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no
+ * @return The {@link ExoTrackSelection.Definition}s for the renderers. A null entry indicates no
* selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
- protected TrackSelection.@NullableType Definition[] selectAllTracks(
+ protected ExoTrackSelection.@NullableType Definition[] selectAllTracks(
MappedTrackInfo mappedTrackInfo,
@Capabilities int[][][] rendererFormatSupports,
@AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,
Parameters params)
throws ExoPlaybackException {
int rendererCount = mappedTrackInfo.getRendererCount();
- TrackSelection.@NullableType Definition[] definitions =
- new TrackSelection.Definition[rendererCount];
+ ExoTrackSelection.@NullableType Definition[] definitions =
+ new ExoTrackSelection.Definition[rendererCount];
boolean seenVideoRendererWithMappedTracks = false;
boolean selectedVideoTracks = false;
@@ -1819,7 +1818,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
boolean enableAdaptiveTrackSelection =
params.allowMultipleAdaptiveSelections || !seenVideoRendererWithMappedTracks;
@Nullable
- Pair audioSelection =
+ Pair audioSelection =
selectAudioTrack(
mappedTrackInfo.getTrackGroups(i),
rendererFormatSupports[i],
@@ -1834,7 +1833,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// score. Clear the selection for that renderer.
definitions[selectedAudioRendererIndex] = null;
}
- TrackSelection.Definition definition = audioSelection.first;
+ ExoTrackSelection.Definition definition = audioSelection.first;
definitions[i] = definition;
// We assume that audio tracks in the same group have matching language.
selectedAudioLanguage = definition.group.getFormat(definition.tracks[0]).language;
@@ -1855,7 +1854,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
break;
case C.TRACK_TYPE_TEXT:
@Nullable
- Pair textSelection =
+ Pair textSelection =
selectTextTrack(
mappedTrackInfo.getTrackGroups(i),
rendererFormatSupports[i],
@@ -1889,7 +1888,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/**
* Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
- * {@link TrackSelection} for a video renderer.
+ * {@link ExoTrackSelection} for a video renderer.
*
* @param groups The {@link TrackGroupArray} mapped to the renderer.
* @param formatSupport The {@link Capabilities} for each mapped track, indexed by track group and
@@ -1898,19 +1897,19 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* adaptation for the renderer.
* @param params The selector's current constraint parameters.
* @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed.
- * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was
+ * @return The {@link ExoTrackSelection.Definition} for the renderer, or null if no selection was
* made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@Nullable
- protected TrackSelection.Definition selectVideoTrack(
+ protected ExoTrackSelection.Definition selectVideoTrack(
TrackGroupArray groups,
@Capabilities int[][] formatSupport,
@AdaptiveSupport int mixedMimeTypeAdaptationSupports,
Parameters params,
boolean enableAdaptiveTrackSelection)
throws ExoPlaybackException {
- TrackSelection.Definition definition = null;
+ ExoTrackSelection.Definition definition = null;
if (!params.forceHighestSupportedBitrate
&& !params.forceLowestBitrate
&& enableAdaptiveTrackSelection) {
@@ -1924,7 +1923,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@Nullable
- private static TrackSelection.Definition selectAdaptiveVideoTrack(
+ private static ExoTrackSelection.Definition selectAdaptiveVideoTrack(
TrackGroupArray groups,
@Capabilities int[][] formatSupport,
@AdaptiveSupport int mixedMimeTypeAdaptationSupports,
@@ -1956,7 +1955,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
params.viewportHeight,
params.viewportOrientationMayChange);
if (adaptiveTracks.length > 0) {
- return new TrackSelection.Definition(group, adaptiveTracks);
+ return new ExoTrackSelection.Definition(group, adaptiveTracks);
}
}
return null;
@@ -1982,8 +1981,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return NO_TRACKS;
}
- List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,
- viewportHeight, viewportOrientationMayChange);
+ List selectedTrackIndices =
+ getViewportFilteredTrackIndices(
+ group, viewportWidth, viewportHeight, viewportOrientationMayChange);
if (selectedTrackIndices.size() < 2) {
return NO_TRACKS;
}
@@ -2140,7 +2140,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
@Nullable
- private static TrackSelection.Definition selectFixedVideoTrack(
+ private static ExoTrackSelection.Definition selectFixedVideoTrack(
TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) {
int selectedTrackIndex = C.INDEX_UNSET;
@Nullable TrackGroup selectedGroup = null;
@@ -2160,8 +2160,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Ignore trick-play tracks for now.
continue;
}
- if (isSupported(trackFormatSupport[trackIndex],
- params.exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(
+ trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) {
VideoTrackScore trackScore =
new VideoTrackScore(
format,
@@ -2183,14 +2183,14 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return selectedGroup == null
? null
- : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ : new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex);
}
// Audio track selection implementation.
/**
* Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
- * {@link TrackSelection} for an audio renderer.
+ * {@link ExoTrackSelection} for an audio renderer.
*
* @param groups The {@link TrackGroupArray} mapped to the renderer.
* @param formatSupport The {@link Capabilities} for each mapped track, indexed by track group and
@@ -2199,13 +2199,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* adaptation for the renderer.
* @param params The selector's current constraint parameters.
* @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed.
- * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or
+ * @return The {@link ExoTrackSelection.Definition} and corresponding {@link AudioTrackScore}, or
* null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@SuppressWarnings("unused")
@Nullable
- protected Pair selectAudioTrack(
+ protected Pair selectAudioTrack(
TrackGroupArray groups,
@Capabilities int[][] formatSupport,
@AdaptiveSupport int mixedMimeTypeAdaptationSupports,
@@ -2219,8 +2219,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup trackGroup = groups.get(groupIndex);
@Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex],
- params.exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(
+ trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
AudioTrackScore trackScore =
new AudioTrackScore(format, params, trackFormatSupport[trackIndex]);
@@ -2243,7 +2243,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup selectedGroup = groups.get(selectedGroupIndex);
- TrackSelection.Definition definition = null;
+ ExoTrackSelection.Definition definition = null;
if (!params.forceHighestSupportedBitrate
&& !params.forceLowestBitrate
&& enableAdaptiveTrackSelection) {
@@ -2258,12 +2258,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
params.allowAudioMixedSampleRateAdaptiveness,
params.allowAudioMixedChannelCountAdaptiveness);
if (adaptiveTracks.length > 1) {
- definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks);
+ definition = new ExoTrackSelection.Definition(selectedGroup, adaptiveTracks);
}
}
if (definition == null) {
// We didn't make an adaptive selection, so make a fixed one instead.
- definition = new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ definition = new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex);
}
return Pair.create(definition, Assertions.checkNotNull(selectedTrackScore));
@@ -2322,7 +2322,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/**
* Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
- * {@link TrackSelection} for a text renderer.
+ * {@link ExoTrackSelection} for a text renderer.
*
* @param groups The {@link TrackGroupArray} mapped to the renderer.
* @param formatSupport The {@link Capabilities} for each mapped track, indexed by track group and
@@ -2330,12 +2330,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @param params The selector's current constraint parameters.
* @param selectedAudioLanguage The language of the selected audio track. May be null if the
* selected text track declares no language or no text track was selected.
- * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null
- * if no selection was made.
+ * @return The {@link ExoTrackSelection.Definition} and corresponding {@link TextTrackScore}, or
+ * null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@Nullable
- protected Pair selectTextTrack(
+ protected Pair selectTextTrack(
TrackGroupArray groups,
@Capabilities int[][] formatSupport,
Parameters params,
@@ -2348,8 +2348,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup trackGroup = groups.get(groupIndex);
@Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex],
- params.exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(
+ trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
TextTrackScore trackScore =
new TextTrackScore(
@@ -2366,7 +2366,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return selectedGroup == null
? null
: Pair.create(
- new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
+ new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
Assertions.checkNotNull(selectedTrackScore));
}
@@ -2374,18 +2374,18 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/**
* Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
- * {@link TrackSelection} for a renderer whose type is neither video, audio or text.
+ * {@link ExoTrackSelection} for a renderer whose type is neither video, audio or text.
*
* @param trackType The type of the renderer.
* @param groups The {@link TrackGroupArray} mapped to the renderer.
* @param formatSupport The {@link Capabilities} for each mapped track, indexed by track group and
* track (in that order).
* @param params The selector's current constraint parameters.
- * @return The {@link TrackSelection} for the renderer, or null if no selection was made.
+ * @return The {@link ExoTrackSelection} for the renderer, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@Nullable
- protected TrackSelection.Definition selectOtherTrack(
+ protected ExoTrackSelection.Definition selectOtherTrack(
int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params)
throws ExoPlaybackException {
@Nullable TrackGroup selectedGroup = null;
@@ -2395,8 +2395,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
TrackGroup trackGroup = groups.get(groupIndex);
@Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
- if (isSupported(trackFormatSupport[trackIndex],
- params.exceedRendererCapabilitiesIfNecessary)) {
+ if (isSupported(
+ trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
OtherTrackScore trackScore = new OtherTrackScore(format, trackFormatSupport[trackIndex]);
if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) {
@@ -2409,7 +2409,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
return selectedGroup == null
? null
- : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ : new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex);
}
// Utility methods.
@@ -2430,7 +2430,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
MappedTrackInfo mappedTrackInfo,
@Capabilities int[][][] renderererFormatSupports,
@NullableType RendererConfiguration[] rendererConfigurations,
- @NullableType TrackSelection[] trackSelections) {
+ @NullableType ExoTrackSelection[] trackSelections) {
// Check whether we can enable tunneling. To enable tunneling we require exactly one audio and
// one video renderer to support tunneling and have a selection.
int tunnelingAudioRendererIndex = -1;
@@ -2438,7 +2438,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
boolean enableTunneling = true;
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
int rendererType = mappedTrackInfo.getRendererType(i);
- TrackSelection trackSelection = trackSelections[i];
+ ExoTrackSelection trackSelection = trackSelections[i];
if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO)
&& trackSelection != null) {
if (rendererSupportsTunneling(
@@ -2471,16 +2471,18 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
- * Returns whether a renderer supports tunneling for a {@link TrackSelection}.
+ * Returns whether a renderer supports tunneling for a {@link ExoTrackSelection}.
*
* @param formatSupport The {@link Capabilities} for each track, indexed by group index and track
* index (in that order).
* @param trackGroups The {@link TrackGroupArray}s for the renderer.
* @param selection The track selection.
- * @return Whether the renderer supports tunneling for the {@link TrackSelection}.
+ * @return Whether the renderer supports tunneling for the {@link ExoTrackSelection}.
*/
private static boolean rendererSupportsTunneling(
- @Capabilities int[][] formatSupport, TrackGroupArray trackGroups, TrackSelection selection) {
+ @Capabilities int[][] formatSupport,
+ TrackGroupArray trackGroups,
+ ExoTrackSelection selection) {
if (selection == null) {
return false;
}
@@ -2565,8 +2567,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return 0;
}
- private static List getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth,
- int viewportHeight, boolean orientationMayChange) {
+ private static List getViewportFilteredTrackIndices(
+ TrackGroup group, int viewportWidth, int viewportHeight, boolean orientationMayChange) {
// Initially include all indices.
ArrayList selectedTrackIndices = new ArrayList<>(group.length);
for (int i = 0; i < group.length; i++) {
@@ -2585,8 +2587,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// smallest to exceed the maximum size at which it can be displayed within the viewport.
// We'll discard formats of higher resolution.
if (format.width > 0 && format.height > 0) {
- Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange,
- viewportWidth, viewportHeight, format.width, format.height);
+ Point maxVideoSizeInViewport =
+ getMaxVideoSizeInViewport(
+ orientationMayChange, viewportWidth, viewportHeight, format.width, format.height);
int videoPixels = format.width * format.height;
if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN)
&& format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN)
@@ -2616,8 +2619,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* Given viewport dimensions and video dimensions, computes the maximum size of the video as it
* will be rendered to fit inside of the viewport.
*/
- private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth,
- int viewportHeight, int videoWidth, int videoHeight) {
+ private static Point getMaxVideoSizeInViewport(
+ boolean orientationMayChange,
+ int viewportWidth,
+ int viewportHeight,
+ int videoWidth,
+ int videoHeight) {
if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) {
// Rotation is allowed, and the video will be larger in the rotated viewport.
int tempViewportWidth = viewportWidth;
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/ExoTrackSelection.java
similarity index 84%
rename from library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java
rename to library/core/src/main/java/com/google/android/exoplayer2/trackselection/ExoTrackSelection.java
index 5a660a0f11..e6816ec884 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/ExoTrackSelection.java
@@ -29,15 +29,12 @@ import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
- * A track selection consisting of a static subset of selected tracks belonging to a {@link
- * TrackGroup}, and a possibly varying individual selected track from the subset.
- *
- * Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual
- * selected track may change dynamically as a result of calling {@link #updateSelectedTrack(long,
- * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)}. This only
- * happens between calls to {@link #enable()} and {@link #disable()}.
+ * A {@link TrackSelection} that can change the individually selected track as a result of calling
+ * {@link #updateSelectedTrack(long, long, long, List, MediaChunkIterator[])} or {@link
+ * #evaluateQueueSize(long, List)}. This only happens between calls to {@link #enable()} and {@link
+ * #disable()}.
*/
-public interface TrackSelection {
+public interface ExoTrackSelection extends TrackSelection {
/** Contains of a subset of selected tracks belonging to a {@link TrackGroup}. */
final class Definition {
@@ -73,7 +70,7 @@ public interface TrackSelection {
}
}
- /** Factory for {@link TrackSelection} instances. */
+ /** Factory for {@link ExoTrackSelection} instances. */
interface Factory {
/**
@@ -91,7 +88,7 @@ public interface TrackSelection {
* include null values.
*/
@NullableType
- TrackSelection[] createTrackSelections(
+ ExoTrackSelection[] createTrackSelections(
@NullableType Definition[] definitions,
BandwidthMeter bandwidthMeter,
MediaPeriodId mediaPeriodId,
@@ -116,50 +113,6 @@ public interface TrackSelection {
*/
void disable();
- /** Returns the {@link TrackGroup} to which the selected tracks belong. */
- TrackGroup getTrackGroup();
-
- // Static subset of selected tracks.
-
- /** Returns the number of tracks in the selection. */
- int length();
-
- /**
- * Returns the format of the track at a given index in the selection.
- *
- * @param index The index in the selection.
- * @return The format of the selected track.
- */
- Format getFormat(int index);
-
- /**
- * Returns the index in the track group of the track at a given index in the selection.
- *
- * @param index The index in the selection.
- * @return The index of the selected track.
- */
- int getIndexInTrackGroup(int index);
-
- /**
- * Returns the index in the selection of the track with the specified format. The format is
- * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) ==
- * index} even if multiple selected tracks have formats that contain the same values.
- *
- * @param format The format.
- * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
- * format is not part of the selection.
- */
- int indexOf(Format format);
-
- /**
- * Returns the index in the selection of the track with the specified index in the track group.
- *
- * @param indexInTrackGroup The index in the track group.
- * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
- * index is not part of the selection.
- */
- int indexOf(int indexInTrackGroup);
-
// Individual selected track.
/** Returns the {@link Format} of the individual selected track. */
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
index 41f36c4970..05ea4bb3c4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -43,14 +43,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s
- * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each
+ * and {@link Renderer}s, and then from that mapping create a {@link ExoTrackSelection} for each
* renderer.
*/
public abstract class MappingTrackSelector extends TrackSelector {
- /**
- * Provides mapped track information for each renderer.
- */
+ /** Provides mapped track information for each renderer. */
public static final class MappedTrackInfo {
/**
@@ -401,7 +399,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
rendererFormatSupports,
unmappedTrackGroupArray);
- Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result =
+ Pair<@NullableType RendererConfiguration[], @NullableType ExoTrackSelection[]> result =
selectTracks(
mappedTrackInfo,
rendererFormatSupports,
@@ -428,7 +426,7 @@ public abstract class MappingTrackSelector extends TrackSelector {
* RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
- protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]>
+ protected abstract Pair<@NullableType RendererConfiguration[], @NullableType ExoTrackSelection[]>
selectTracks(
MappedTrackInfo mappedTrackInfo,
@Capabilities int[][][] rendererFormatSupports,
@@ -538,5 +536,4 @@ public abstract class MappingTrackSelector extends TrackSelector {
}
return mixedMimeTypeAdaptationSupport;
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
index 5f0ab76d6f..3dcb73de21 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
@@ -28,15 +28,11 @@ import java.util.List;
import java.util.Random;
import org.checkerframework.checker.nullness.compatqual.NullableType;
-/**
- * A {@link TrackSelection} whose selected track is updated randomly.
- */
+/** An {@link ExoTrackSelection} whose selected track is updated randomly. */
public final class RandomTrackSelection extends BaseTrackSelection {
- /**
- * Factory for {@link RandomTrackSelection} instances.
- */
- public static final class Factory implements TrackSelection.Factory {
+ /** Factory for {@link RandomTrackSelection} instances. */
+ public static final class Factory implements ExoTrackSelection.Factory {
private final Random random;
@@ -44,15 +40,13 @@ public final class RandomTrackSelection extends BaseTrackSelection {
random = new Random();
}
- /**
- * @param seed A seed for the {@link Random} instance used by the factory.
- */
+ /** @param seed A seed for the {@link Random} instance used by the factory. */
public Factory(int seed) {
random = new Random(seed);
}
@Override
- public @NullableType TrackSelection[] createTrackSelections(
+ public @NullableType ExoTrackSelection[] createTrackSelections(
@NullableType Definition[] definitions,
BandwidthMeter bandwidthMeter,
MediaPeriodId mediaPeriodId,
@@ -144,5 +138,4 @@ public final class RandomTrackSelection extends BaseTrackSelection {
public Object getSelectionData() {
return null;
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
index 0f2748b1ac..0dac7259a3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
@@ -18,7 +18,7 @@ package com.google.android.exoplayer2.trackselection;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
-import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Track selection related utility methods. */
@@ -35,7 +35,7 @@ public final class TrackSelectionUtil {
* @param trackSelectionDefinition A {@link Definition} for the track selection.
* @return The created track selection.
*/
- TrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition);
+ ExoTrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition);
}
/**
@@ -48,10 +48,10 @@ public final class TrackSelectionUtil {
* @return The array of created track selection. For null entries in {@code definitions} returns
* null values.
*/
- public static @NullableType TrackSelection[] createTrackSelectionsForDefinitions(
+ public static @NullableType ExoTrackSelection[] createTrackSelectionsForDefinitions(
@NullableType Definition[] definitions,
AdaptiveTrackSelectionFactory adaptiveTrackSelectionFactory) {
- TrackSelection[] selections = new TrackSelection[definitions.length];
+ ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length];
boolean createdAdaptiveTrackSelection = false;
for (int i = 0; i < definitions.length; i++) {
Definition definition = definitions[i];
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
index f3d59d537e..59c5d5447b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java
@@ -83,7 +83,7 @@ import com.google.android.exoplayer2.util.Assertions;
* thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()}
* from any thread.
*/
-public abstract class TrackSelector implements TrackSelectorInterface {
+public abstract class TrackSelector {
/**
* Notified when selections previously made by a {@link TrackSelector} are no longer valid.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
index 67623c2cf6..e7f0caaedf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
@@ -20,9 +20,7 @@ import com.google.android.exoplayer2.RendererConfiguration;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.compatqual.NullableType;
-/**
- * The result of a {@link TrackSelector} operation.
- */
+/** The result of a {@link TrackSelector} operation. */
public final class TrackSelectorResult {
/** The number of selections in the result. Greater than or equal to zero. */
@@ -32,10 +30,8 @@ public final class TrackSelectorResult {
* renderer should be disabled.
*/
public final @NullableType RendererConfiguration[] rendererConfigurations;
- /**
- * A {@link TrackSelectionArray} containing the track selection for each renderer.
- */
- public final TrackSelectionArray selections;
+ /** A {@link ExoTrackSelection} array containing the track selection for each renderer. */
+ public final @NullableType ExoTrackSelection[] selections;
/**
* An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
* should the selections be activated.
@@ -45,17 +41,17 @@ public final class TrackSelectorResult {
/**
* @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry
* indicates the corresponding renderer should be disabled.
- * @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
+ * @param selections A {@link ExoTrackSelection} array containing the selection for each renderer.
* @param info An opaque object that will be returned to {@link
* TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be
* {@code null}.
*/
public TrackSelectorResult(
@NullableType RendererConfiguration[] rendererConfigurations,
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
@Nullable Object info) {
this.rendererConfigurations = rendererConfigurations;
- this.selections = new TrackSelectionArray(selections);
+ this.selections = selections.clone();
this.info = info;
length = rendererConfigurations.length;
}
@@ -100,7 +96,6 @@ public final class TrackSelectorResult {
return false;
}
return Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index])
- && Util.areEqual(selections.get(index), other.selections.get(index));
+ && Util.areEqual(selections[index], other.selections[index]);
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java
index d520fcfa60..f35d745892 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/BandwidthMeter.java
@@ -35,7 +35,7 @@ public interface BandwidthMeter {
* changed.
*
*
Note: The estimated bitrate is typically derived from more information than just {@code
- * bytes} and {@code elapsedMs}.
+ * bytesTransferred} and {@code elapsedMs}.
*
* @param elapsedMs The time taken to transfer {@code bytesTransferred}, in milliseconds. This
* is at most the elapsed time since the last callback, but may be less if there were
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
index 2c3670f52a..2b9cf00e47 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java
@@ -59,7 +59,6 @@ public final class DataSchemeDataSource extends BaseDataSource {
String dataString = uriParts[1];
if (uriParts[0].contains(";base64")) {
try {
- // TODO(internal: b/169937045): Consider passing Base64.URL_SAFE flag.
data = Base64.decode(dataString, /* flags= */ Base64.DEFAULT);
} catch (IllegalArgumentException e) {
throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
index c917929111..eb782bd334 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java
@@ -178,10 +178,10 @@ public interface Cache {
* @param key The cache key of the resource.
* @param position The starting position in the resource from which data is required.
* @param length The length of the data being requested, or {@link C#LENGTH_UNSET} if unbounded.
- * The length is ignored in the case of a cache hit. In the case of a cache miss, it defines
- * the maximum length of the hole {@link CacheSpan} that's returned. Cache implementations may
- * support parallel writes into non-overlapping holes, and so passing the actual required
- * length should be preferred to passing {@link C#LENGTH_UNSET} when possible.
+ * The length is ignored if there is a cache entry that overlaps the position. Else, it
+ * defines the maximum length of the hole {@link CacheSpan} that's returned. Cache
+ * implementations may support parallel writes into non-overlapping holes, and so passing the
+ * actual required length should be preferred to passing {@link C#LENGTH_UNSET} when possible.
* @return The {@link CacheSpan}.
* @throws InterruptedException If the thread was interrupted.
* @throws CacheException If an error is encountered.
@@ -199,8 +199,8 @@ public interface Cache {
* @param key The cache key of the resource.
* @param position The starting position in the resource from which data is required.
* @param length The length of the data being requested, or {@link C#LENGTH_UNSET} if unbounded.
- * The length is ignored in the case of a cache hit. In the case of a cache miss, it defines
- * the range of data locked by the returned {@link CacheSpan}.
+ * The length is ignored if there is a cache entry that overlaps the position. Else, it
+ * defines the range of data locked by the returned {@link CacheSpan}.
* @return The {@link CacheSpan}. Or null if the cache entry is locked.
* @throws CacheException If an error is encountered.
*/
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 cc1e5a8e5e..0f1da88115 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
@@ -837,7 +837,8 @@ public final class SimpleCache implements Cache {
}
private static void createCacheDirectories(File cacheDir) throws CacheException {
- if (!cacheDir.mkdirs()) {
+ // If mkdirs() returns false, double check that the directory doesn't exist before throwing.
+ if (!cacheDir.mkdirs() && !cacheDir.isDirectory()) {
String message = "Failed to create cache directory: " + cacheDir;
Log.e(TAG, message);
throw new CacheException(message);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
deleted file mode 100644
index dc0f93165a..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.util;
-
-import android.os.Looper;
-import android.os.Message;
-import androidx.annotation.Nullable;
-
-/** The standard implementation of {@link HandlerWrapper}. */
-/* package */ final class SystemHandlerWrapper implements HandlerWrapper {
-
- private final android.os.Handler handler;
-
- public SystemHandlerWrapper(android.os.Handler handler) {
- this.handler = handler;
- }
-
- @Override
- public Looper getLooper() {
- return handler.getLooper();
- }
-
- @Override
- public Message obtainMessage(int what) {
- return handler.obtainMessage(what);
- }
-
- @Override
- public Message obtainMessage(int what, @Nullable Object obj) {
- return handler.obtainMessage(what, obj);
- }
-
- @Override
- public Message obtainMessage(int what, int arg1, int arg2) {
- return handler.obtainMessage(what, arg1, arg2);
- }
-
- @Override
- public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) {
- return handler.obtainMessage(what, arg1, arg2, obj);
- }
-
- @Override
- public boolean sendEmptyMessage(int what) {
- return handler.sendEmptyMessage(what);
- }
-
- @Override
- public boolean sendEmptyMessageDelayed(int what, int delayMs) {
- return handler.sendEmptyMessageDelayed(what, delayMs);
- }
-
- @Override
- public boolean sendEmptyMessageAtTime(int what, long uptimeMs) {
- return handler.sendEmptyMessageAtTime(what, uptimeMs);
- }
-
- @Override
- public void removeMessages(int what) {
- handler.removeMessages(what);
- }
-
- @Override
- public void removeCallbacksAndMessages(@Nullable Object token) {
- handler.removeCallbacksAndMessages(token);
- }
-
- @Override
- public boolean post(Runnable runnable) {
- return handler.post(runnable);
- }
-
- @Override
- public boolean postDelayed(Runnable runnable, long delayMs) {
- return handler.postDelayed(runnable, delayMs);
- }
-}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
index 241da059ab..1cebbbd011 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
@@ -20,7 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.DefaultLoadControl.Builder;
import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import org.junit.Before;
import org.junit.Test;
@@ -177,7 +177,7 @@ public class DefaultLoadControlTest {
@Test
public void shouldContinueLoading_withNoSelectedTracks_returnsTrue() {
loadControl = builder.build();
- loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new TrackSelectionArray());
+ loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new ExoTrackSelection[0]);
assertThat(
loadControl.shouldContinueLoading(
@@ -321,5 +321,4 @@ public class DefaultLoadControlTest {
allocator.allocate();
}
}
-
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
index 55c3bd70ed..4077b8f5b8 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -115,6 +115,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -130,7 +131,6 @@ import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.shadows.ShadowAudioManager;
-import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link ExoPlayer}. */
@RunWith(AndroidJUnit4.class)
@@ -1003,11 +1003,15 @@ public final class ExoPlayerTest {
.waitForPlaybackState(Player.STATE_BUFFERING)
// Block until createPeriod has been called on the fake media source.
.executeRunnable(
- () -> {
- try {
- createPeriodCalledCountDownLatch.await();
- } catch (InterruptedException e) {
- throw new IllegalStateException(e);
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ try {
+ player.getClock().onThreadBlocked();
+ createPeriodCalledCountDownLatch.await();
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
}
})
// Set playback speed (while the fake media period is not yet prepared).
@@ -2818,6 +2822,7 @@ public final class ExoPlayerTest {
// seek in the timeline which still has two windows in EPI, but when the seek
// arrives in EPII the actual timeline has one window only. Hence it tries to
// find the subsequent period of the removed period and finds it.
+ player.getClock().onThreadBlocked();
sourceReleasedCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
@@ -4240,7 +4245,7 @@ public final class ExoPlayerTest {
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(windowIndex[0]).isEqualTo(0);
- assertThat(positionMs[0]).isGreaterThan(8000);
+ assertThat(positionMs[0]).isEqualTo(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
@@ -4467,7 +4472,7 @@ public final class ExoPlayerTest {
assertThat(windowIndex[2]).isEqualTo(0);
assertThat(isPlayingAd[2]).isFalse();
- assertThat(positionMs[2]).isGreaterThan(8000);
+ assertThat(positionMs[2]).isEqualTo(8000);
assertThat(bufferedPositionMs[2]).isEqualTo(contentDurationMs);
assertThat(totalBufferedDurationMs[2]).isAtLeast(contentDurationMs - positionMs[2]);
}
@@ -4591,65 +4596,30 @@ public final class ExoPlayerTest {
@Test
public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception {
- CountDownLatch becomingNoisyHandlingDisabled = new CountDownLatch(1);
- CountDownLatch becomingNoisyDelivered = new CountDownLatch(1);
- PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber();
- ActionSchedule actionSchedule =
- new ActionSchedule.Builder(TAG)
- .executeRunnable(
- new PlayerRunnable() {
- @Override
- public void run(SimpleExoPlayer player) {
- player.setHandleAudioBecomingNoisy(false);
- becomingNoisyHandlingDisabled.countDown();
+ SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
+ player.play();
- // Wait for the broadcast to be delivered from the main thread.
- try {
- becomingNoisyDelivered.await();
- } catch (InterruptedException e) {
- throw new IllegalStateException(e);
- }
- }
- })
- .delay(1) // Handle pending messages on the playback thread.
- .executeRunnable(playerStateGrabber)
- .build();
-
- ExoPlayerTestRunner testRunner =
- new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start();
- becomingNoisyHandlingDisabled.await();
+ player.setHandleAudioBecomingNoisy(false);
deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
- becomingNoisyDelivered.countDown();
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
+ boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady();
+ player.release();
- testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
- assertThat(playerStateGrabber.playWhenReady).isTrue();
+ assertThat(playWhenReadyAfterBroadcast).isTrue();
}
@Test
public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception {
- CountDownLatch becomingNoisyHandlingEnabled = new CountDownLatch(1);
- ActionSchedule actionSchedule =
- new ActionSchedule.Builder(TAG)
- .executeRunnable(
- new PlayerRunnable() {
- @Override
- public void run(SimpleExoPlayer player) {
- player.setHandleAudioBecomingNoisy(true);
- becomingNoisyHandlingEnabled.countDown();
- }
- })
- .waitForPlayWhenReady(false) // Becoming noisy should set playWhenReady = false
- .play()
- .build();
+ SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
+ player.play();
- ExoPlayerTestRunner testRunner =
- new ExoPlayerTestRunner.Builder(context).setActionSchedule(actionSchedule).build().start();
- becomingNoisyHandlingEnabled.await();
+ player.setHandleAudioBecomingNoisy(true);
deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
+ boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady();
+ player.release();
- // If the player fails to handle becoming noisy, blockUntilActionScheduleFinished will time out
- // and throw, causing the test to fail.
- testRunner.blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
+ assertThat(playWhenReadyAfterBroadcast).isFalse();
}
@Test
@@ -4675,7 +4645,7 @@ public final class ExoPlayerTest {
// Use chunked data to ensure the player actually needs to continue loading and playing.
FakeAdaptiveDataSet.Factory dataSetFactory =
new FakeAdaptiveDataSet.Factory(
- /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0);
+ /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0, new Random(0));
MediaSource chunkedMediaSource =
new FakeAdaptiveMediaSource(
new FakeTimeline(),
@@ -4800,7 +4770,7 @@ public final class ExoPlayerTest {
// Use chunked data to ensure the player actually needs to continue loading and playing.
FakeAdaptiveDataSet.Factory dataSetFactory =
new FakeAdaptiveDataSet.Factory(
- /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0);
+ /* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0, new Random(0));
MediaSource chunkedMediaSource =
new FakeAdaptiveMediaSource(
new FakeTimeline(),
@@ -4816,6 +4786,53 @@ public final class ExoPlayerTest {
.blockUntilEnded(TIMEOUT_MS);
}
+ @Test
+ public void shortAdFollowedByUnpreparedAd_playbackDoesNotGetStuck() throws Exception {
+ AdPlaybackState adPlaybackState =
+ FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 2, /* adGroupTimesUs...= */ 0);
+ long shortAdDurationMs = 1_000;
+ adPlaybackState =
+ adPlaybackState.withAdDurationsUs(new long[][] {{shortAdDurationMs, shortAdDurationMs}});
+ Timeline timeline =
+ new FakeTimeline(
+ new TimelineWindowDefinition(
+ /* periodCount= */ 1,
+ /* id= */ 0,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* durationUs= */ C.msToUs(10000),
+ adPlaybackState));
+ // Simulate the second ad not being prepared.
+ FakeMediaSource mediaSource =
+ new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
+ @Override
+ protected MediaPeriod createMediaPeriod(
+ MediaPeriodId id,
+ TrackGroupArray trackGroupArray,
+ Allocator allocator,
+ MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
+ DrmSessionManager drmSessionManager,
+ DrmSessionEventListener.EventDispatcher drmEventDispatcher,
+ @Nullable TransferListener transferListener) {
+ return new FakeMediaPeriod(
+ trackGroupArray,
+ allocator,
+ FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(0),
+ mediaSourceEventDispatcher,
+ drmSessionManager,
+ drmEventDispatcher,
+ /* deferOnPrepared= */ id.adIndexInAdGroup == 1);
+ }
+ };
+ SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
+ player.setMediaSource(mediaSource);
+ player.prepare();
+ player.play();
+
+ // The player is not stuck in the buffering state.
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
+ }
+
@Test
public void moveMediaItem() throws Exception {
TimelineWindowDefinition firstWindowDefinition =
@@ -4838,8 +4855,8 @@ public final class ExoPlayerTest {
MediaSource mediaSource2 = new FakeMediaSource(timeline2);
Timeline expectedPlaceholderTimeline =
new FakeTimeline(
- TimelineWindowDefinition.createDummy(/* tag= */ 1),
- TimelineWindowDefinition.createDummy(/* tag= */ 2));
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 1),
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 2));
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged(
@@ -4911,9 +4928,9 @@ public final class ExoPlayerTest {
Timeline expectedPlaceholderTimeline =
new FakeTimeline(
- TimelineWindowDefinition.createDummy(/* tag= */ 1),
- TimelineWindowDefinition.createDummy(/* tag= */ 2),
- TimelineWindowDefinition.createDummy(/* tag= */ 3));
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 1),
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 2),
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 3));
Timeline expectedRealTimeline =
new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition);
Timeline expectedRealTimelineAfterRemove =
@@ -4971,9 +4988,9 @@ public final class ExoPlayerTest {
Timeline expectedPlaceholderTimeline =
new FakeTimeline(
- TimelineWindowDefinition.createDummy(/* tag= */ 1),
- TimelineWindowDefinition.createDummy(/* tag= */ 2),
- TimelineWindowDefinition.createDummy(/* tag= */ 3));
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 1),
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 2),
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 3));
Timeline expectedRealTimeline =
new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition);
Timeline expectedRealTimelineAfterRemove = new FakeTimeline(firstWindowDefinition);
@@ -5108,8 +5125,8 @@ public final class ExoPlayerTest {
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source update after prepare */);
Timeline expectedSecondPlaceholderTimeline =
new FakeTimeline(
- TimelineWindowDefinition.createDummy(/* tag= */ 0),
- TimelineWindowDefinition.createDummy(/* tag= */ 0));
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 0),
+ TimelineWindowDefinition.createPlaceholder(/* tag= */ 0));
Timeline expectedSecondRealTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
@@ -6952,7 +6969,7 @@ public final class ExoPlayerTest {
}, // buffers after set items with seek
maskingPlaybackStates);
assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentWindowIndices);
- assertThat(currentPositions[0]).isGreaterThan(0);
+ assertThat(currentPositions[0]).isEqualTo(0);
assertThat(currentPositions[1]).isEqualTo(0);
assertThat(currentPositions[2]).isEqualTo(0);
assertThat(bufferedPositions[0]).isGreaterThan(0);
@@ -7566,7 +7583,7 @@ public final class ExoPlayerTest {
FakeMediaSource firstMediaSource =
new FakeMediaSource(
/* timeline= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
(unusedFormat, unusedMediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM),
@@ -7574,7 +7591,7 @@ public final class ExoPlayerTest {
FakeMediaSource secondMediaSource =
new FakeMediaSource(
timelineWithOffsets,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
(unusedFormat, unusedMediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM),
@@ -8108,7 +8125,7 @@ public final class ExoPlayerTest {
allocator,
/* singleSampleTimeUs= */ 0,
mediaSourceEventDispatcher,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
drmEventDispatcher,
/* deferOnPrepared= */ true) {
@Override
@@ -8840,7 +8857,7 @@ public final class ExoPlayerTest {
player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formatWithStaticMetadata));
player.seekTo(2_000);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f));
- ShadowLooper.runMainLooperToNextTask();
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
verify(listener).onTimelineChanged(any(), anyInt());
verify(listener).onMediaItemTransition(any(), anyInt());
@@ -8863,7 +8880,7 @@ public final class ExoPlayerTest {
}
});
player.setRepeatMode(Player.REPEAT_MODE_ONE);
- ShadowLooper.runMainLooperToNextTask();
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
verify(listener).onRepeatModeChanged(anyInt());
verify(listener).onShuffleModeEnabledChanged(anyBoolean());
@@ -8879,7 +8896,7 @@ public final class ExoPlayerTest {
player.play();
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE);
- ShadowLooper.runMainLooperToNextTask();
+ TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
player.release();
// Verify that all callbacks have been called at least once.
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java
index 6a6a2dcf42..e3067d8e25 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java
@@ -31,7 +31,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.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -470,7 +470,7 @@ public final class MediaPeriodQueueTest {
mediaSourceList,
getNextMediaPeriodInfo(),
new TrackSelectorResult(
- new RendererConfiguration[0], new TrackSelection[0], /* info= */ null));
+ new RendererConfiguration[0], new ExoTrackSelection[0], /* info= */ null));
}
private MediaPeriodInfo getNextMediaPeriodInfo() {
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java
index 53f6c24f10..154ec0df1b 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/MetadataRetrieverTest.java
@@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.shadows.ShadowLooper;
/** Tests for {@link MetadataRetriever}. */
@RunWith(AndroidJUnit4.class)
@@ -63,6 +64,7 @@ public class MetadataRetrieverTest {
ListenableFuture trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
+ ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(2);
@@ -85,6 +87,7 @@ public class MetadataRetrieverTest {
retrieveMetadata(context, mediaItem1, clock);
ListenableFuture trackGroupsFuture2 =
retrieveMetadata(context, mediaItem2, clock);
+ ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups1 = trackGroupsFuture1.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
TrackGroupArray trackGroups2 = trackGroupsFuture2.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
@@ -118,6 +121,7 @@ public class MetadataRetrieverTest {
ListenableFuture trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
+ ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(1);
@@ -134,6 +138,7 @@ public class MetadataRetrieverTest {
ListenableFuture trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
+ ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(1);
@@ -164,6 +169,7 @@ public class MetadataRetrieverTest {
ListenableFuture trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
+ ShadowLooper.idleMainLooper();
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(2); // Video and audio
@@ -185,6 +191,7 @@ public class MetadataRetrieverTest {
ListenableFuture trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
+ ShadowLooper.idleMainLooper();
assertThrows(
ExecutionException.class, () -> trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS));
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java
index 70fd5445e1..41579f073c 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/PlayerMessageTest.java
@@ -22,7 +22,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
-import android.os.Handler;
import android.os.HandlerThread;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.Clock;
@@ -55,9 +54,14 @@ public class PlayerMessageTest {
PlayerMessage.Target target = (messageType, payload) -> {};
handlerThread = new HandlerThread("TestHandler");
handlerThread.start();
- Handler handler = new Handler(handlerThread.getLooper());
message =
- new PlayerMessage(sender, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler);
+ new PlayerMessage(
+ sender,
+ target,
+ Timeline.EMPTY,
+ /* defaultWindowIndex= */ 0,
+ clock,
+ handlerThread.getLooper());
}
@After
@@ -69,8 +73,7 @@ public class PlayerMessageTest {
public void blockUntilDelivered_timesOut() throws Exception {
when(clock.elapsedRealtime()).thenReturn(0L).thenReturn(TIMEOUT_MS * 2);
- assertThrows(
- TimeoutException.class, () -> message.send().blockUntilDelivered(TIMEOUT_MS, clock));
+ assertThrows(TimeoutException.class, () -> message.send().blockUntilDelivered(TIMEOUT_MS));
// Ensure blockUntilDelivered() entered the blocking loop.
verify(clock, Mockito.times(2)).elapsedRealtime();
@@ -82,7 +85,7 @@ public class PlayerMessageTest {
message.send().markAsProcessed(/* isDelivered= */ true);
- assertThat(message.blockUntilDelivered(TIMEOUT_MS, clock)).isTrue();
+ assertThat(message.blockUntilDelivered(TIMEOUT_MS)).isTrue();
}
@Test
@@ -110,7 +113,7 @@ public class PlayerMessageTest {
});
try {
- assertThat(message.blockUntilDelivered(TIMEOUT_MS, clock)).isTrue();
+ assertThat(message.blockUntilDelivered(TIMEOUT_MS)).isTrue();
// Ensure blockUntilDelivered() entered the blocking loop.
verify(clock, Mockito.atLeast(2)).elapsedRealtime();
future.get(1, SECONDS);
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java
index 9dfd643712..fe3edf4177 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java
@@ -40,12 +40,13 @@ public class TimelineTest {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111));
TimelineAsserts.assertWindowTags(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, C.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(timeline, C.REPEAT_MODE_ONE, false, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, C.REPEAT_MODE_ALL, false, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, C.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, C.REPEAT_MODE_ONE, false, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, C.REPEAT_MODE_ALL, false, 0);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
}
@Test
@@ -53,12 +54,13 @@ public class TimelineTest {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111));
TimelineAsserts.assertWindowTags(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 5);
- TimelineAsserts.assertPreviousWindowIndices(timeline, C.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(timeline, C.REPEAT_MODE_ONE, false, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, C.REPEAT_MODE_ALL, false, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, C.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, C.REPEAT_MODE_ONE, false, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, C.REPEAT_MODE_ALL, false, 0);
+ TimelineAsserts.assertPreviousWindowIndices(
+ timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
+ TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
+ TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
}
@Test
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java
index ffdc65160e..bc7d149007 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java
@@ -215,7 +215,7 @@ public final class AnalyticsCollectorTest {
period0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
- .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */)
+ .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */)
.inOrder();
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0 /* started */, period0 /* stopped */)
@@ -656,9 +656,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGE */,
- WINDOW_0 /* SOURCE_UPDATE */,
+ period0Seq0 /* SOURCE_UPDATE */,
WINDOW_0 /* PLAYLIST_CHANGE */,
- WINDOW_0 /* SOURCE_UPDATE */);
+ period0Seq1 /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1)
.inOrder();
@@ -748,7 +748,7 @@ public final class AnalyticsCollectorTest {
period0Seq0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
- .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */);
+ .containsExactly(WINDOW_0 /* prepared */, period0Seq0 /* prepared */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0);
@@ -929,7 +929,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
- WINDOW_0 /* SOURCE_UPDATE (first item) */,
+ period0Seq0 /* SOURCE_UPDATE (first item) */,
period0Seq0 /* PLAYLIST_CHANGED (add) */,
period0Seq0 /* SOURCE_UPDATE (second item) */,
period0Seq1 /* PLAYLIST_CHANGED (remove) */)
@@ -949,7 +949,7 @@ public final class AnalyticsCollectorTest {
.containsExactly(period0Seq0, period1Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_DECODER_ENABLED))
- .containsExactly(period0Seq0, period1Seq1, period0Seq1)
+ .containsExactly(period0Seq0, period0Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_DECODER_INIT))
.containsExactly(period0Seq0, period1Seq1)
@@ -957,10 +957,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED))
.containsExactly(period0Seq0, period1Seq1)
.inOrder();
- assertThat(listener.getEvents(EVENT_DECODER_DISABLED))
- .containsExactly(period0Seq0, period0Seq0);
+ assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_ENABLED))
- .containsExactly(period0Seq0, period1Seq1, period0Seq1)
+ .containsExactly(period0Seq0, period0Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED))
.containsExactly(period0Seq0, period1Seq1)
@@ -968,13 +967,13 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED))
.containsExactly(period0Seq0, period1Seq1)
.inOrder();
- assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0, period0Seq0);
+ assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
- .containsExactly(period0Seq0, period0Seq1)
+ .containsExactly(period0Seq0, period1Seq1, period0Seq1)
.inOrder();
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
- .containsExactly(period0Seq0, period0Seq1);
+ .containsExactly(period0Seq0, period1Seq1, period0Seq1);
assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET))
.containsExactly(period0Seq1);
listener.assertNoMoreEvents();
@@ -1005,7 +1004,7 @@ public final class AnalyticsCollectorTest {
FakeMediaSource fakeMediaSource =
new FakeMediaSource(
adTimeline,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
(unusedFormat, mediaPeriodId) -> {
if (mediaPeriodId.isAd()) {
return ImmutableList.of(
@@ -1132,7 +1131,7 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
- WINDOW_0 /* SOURCE_UPDATE (initial) */,
+ prerollAd /* SOURCE_UPDATE (initial) */,
contentAfterPreroll /* SOURCE_UPDATE (played preroll) */,
contentAfterMidroll /* SOURCE_UPDATE (played midroll) */,
contentAfterPostroll /* SOURCE_UPDATE (played postroll) */)
@@ -1265,7 +1264,7 @@ public final class AnalyticsCollectorTest {
FakeMediaSource fakeMediaSource =
new FakeMediaSource(
adTimeline,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
(unusedFormat, mediaPeriodId) -> {
if (mediaPeriodId.isAd()) {
return ImmutableList.of(
@@ -1327,7 +1326,7 @@ public final class AnalyticsCollectorTest {
contentAfterMidroll /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
- .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */);
+ .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, contentBeforeMidroll /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(
contentAfterMidroll /* seek */,
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
index c736444a43..bd5dfb97a5 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
@@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.analytics;
+import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
@@ -42,6 +42,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link PlaybackStatsListener}. */
@RunWith(AndroidJUnit4.class)
@@ -60,41 +61,41 @@ public final class PlaybackStatsListenerTest {
}
@Test
- public void events_duringInitialIdleState_dontCreateNewPlaybackStats() {
+ public void events_duringInitialIdleState_dontCreateNewPlaybackStats() throws Exception {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
player.addAnalyticsListener(playbackStatsListener);
player.seekTo(/* positionMs= */ 1234);
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f));
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
player.play();
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
assertThat(playbackStatsListener.getPlaybackStats()).isNull();
}
@Test
- public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() {
+ public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() throws Exception {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
player.addAnalyticsListener(playbackStatsListener);
player.prepare();
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
}
@Test
- public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() {
+ public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() throws Exception {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
player.addAnalyticsListener(playbackStatsListener);
player.setMediaItem(MediaItem.fromUri("http://test.org"));
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
}
@@ -109,7 +110,7 @@ public final class PlaybackStatsListenerTest {
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
assertThat(playbackStats).isNotNull();
@@ -126,7 +127,7 @@ public final class PlaybackStatsListenerTest {
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
assertThat(playbackStats).isNotNull();
@@ -134,7 +135,7 @@ public final class PlaybackStatsListenerTest {
}
@Test
- public void finishedSession_callsCallback() {
+ public void finishedSession_callsCallback() throws Exception {
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, callback);
@@ -143,10 +144,10 @@ public final class PlaybackStatsListenerTest {
// Create session with some events and finish it by removing it from the playlist.
player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)));
player.prepare();
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
verify(callback, never()).onPlaybackStatsReady(any(), any());
player.clearMediaItems();
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
verify(callback).onPlaybackStatsReady(any(), any());
}
@@ -166,9 +167,9 @@ public final class PlaybackStatsListenerTest {
// the first one isn't finished yet.
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ player.getDuration());
- runMainLooperToNextTask();
+ runUntilPendingCommandsAreFullyHandled(player);
player.release();
- runMainLooperToNextTask();
+ ShadowLooper.idleMainLooper();
ArgumentCaptor eventTimeCaptor =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java
index db91637ca9..f24e09346f 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DecoderAudioRendererTest.java
@@ -107,7 +107,7 @@ public class DecoderAudioRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
FORMAT,
ImmutableList.of(END_OF_STREAM_ITEM));
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java
index e39769f2c3..c69deeaeef 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/MediaCodecAudioRendererTest.java
@@ -121,7 +121,7 @@ public class MediaCodecAudioRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ AUDIO_AAC,
ImmutableList.of(
@@ -178,7 +178,7 @@ public class MediaCodecAudioRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ AUDIO_AAC,
ImmutableList.of(
@@ -256,7 +256,7 @@ public class MediaCodecAudioRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ AUDIO_AAC,
ImmutableList.of(
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java
index a700350b0b..5ac26a76b3 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -24,7 +25,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil;
-import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.util.UUID;
@@ -61,10 +61,11 @@ public class DefaultDrmSessionManagerTest {
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession drmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(drmSession);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
@@ -84,10 +85,11 @@ public class DefaultDrmSessionManagerTest {
drmSessionManager.prepare();
DrmSession drmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(drmSession);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
@@ -109,10 +111,11 @@ public class DefaultDrmSessionManagerTest {
drmSessionManager.prepare();
DrmSession drmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(drmSession);
drmSession.release(/* eventDispatcher= */ null);
@@ -131,10 +134,11 @@ public class DefaultDrmSessionManagerTest {
drmSessionManager.prepare();
DrmSession drmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(drmSession);
drmSession.release(/* eventDispatcher= */ null);
@@ -161,10 +165,11 @@ public class DefaultDrmSessionManagerTest {
drmSessionManager.prepare();
DrmSession firstDrmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(firstDrmSession);
firstDrmSession.release(/* eventDispatcher= */ null);
@@ -172,10 +177,11 @@ public class DefaultDrmSessionManagerTest {
// drmSessionManager's internal reference.
assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
DrmSession secondDrmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- secondFormatWithDrmInitData);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ secondFormatWithDrmInitData));
// The drmSessionManager had to release firstDrmSession in order to acquire secondDrmSession.
assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
@@ -195,10 +201,11 @@ public class DefaultDrmSessionManagerTest {
drmSessionManager.prepare();
DrmSession firstDrmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(firstDrmSession);
firstDrmSession.release(/* eventDispatcher= */ null);
@@ -207,10 +214,11 @@ public class DefaultDrmSessionManagerTest {
// Acquire a session for the same init data 5s in to the 10s timeout (so expect the same
// instance).
DrmSession secondDrmSession =
- drmSessionManager.acquireSession(
- /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
- /* eventDispatcher= */ null,
- FORMAT_WITH_DRM_INIT_DATA);
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
assertThat(secondDrmSession).isSameInstanceAs(firstDrmSession);
// Let the timeout definitely expire, and check the session didn't get released.
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/PlaylistPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/PlaylistPlaybackTest.java
new file mode 100644
index 0000000000..3e6d1cfb12
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/PlaylistPlaybackTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.e2etest;
+
+import android.content.Context;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.robolectric.PlaybackOutput;
+import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
+import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
+import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
+import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
+import com.google.android.exoplayer2.testutil.DumpFileAsserts;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** End-to-end tests for playlists. */
+// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
+@Config(sdk = 29)
+@RunWith(AndroidJUnit4.class)
+public final class PlaylistPlaybackTest {
+
+ @Rule
+ public ShadowMediaCodecConfig mediaCodecConfig =
+ ShadowMediaCodecConfig.forAllSupportedMimeTypes();
+
+ @Test
+ public void test_bypassOnThenOn() throws Exception {
+ Context applicationContext = ApplicationProvider.getApplicationContext();
+ CapturingRenderersFactory capturingRenderersFactory =
+ new CapturingRenderersFactory(applicationContext);
+ SimpleExoPlayer player =
+ new SimpleExoPlayer.Builder(applicationContext, capturingRenderersFactory)
+ .setClock(new AutoAdvancingFakeClock())
+ .build();
+ PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
+
+ player.addMediaItem(MediaItem.fromUri("asset:///media/wav/sample.wav"));
+ player.addMediaItem(MediaItem.fromUri("asset:///media/mka/bear-opus.mka"));
+ player.prepare();
+ player.play();
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
+ player.release();
+
+ DumpFileAsserts.assertOutput(
+ applicationContext, playbackOutput, "playbackdumps/playlists/bypass-on-then-off.dump");
+ }
+
+ @Test
+ public void test_bypassOffThenOn() throws Exception {
+ Context applicationContext = ApplicationProvider.getApplicationContext();
+ CapturingRenderersFactory capturingRenderersFactory =
+ new CapturingRenderersFactory(applicationContext);
+ SimpleExoPlayer player =
+ new SimpleExoPlayer.Builder(applicationContext, capturingRenderersFactory)
+ .setClock(new AutoAdvancingFakeClock())
+ .build();
+ PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
+
+ player.addMediaItem(MediaItem.fromUri("asset:///media/mka/bear-opus.mka"));
+ player.addMediaItem(MediaItem.fromUri("asset:///media/wav/sample.wav"));
+ player.prepare();
+ player.play();
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
+ player.release();
+
+ DumpFileAsserts.assertOutput(
+ applicationContext, playbackOutput, "playbackdumps/playlists/bypass-off-then-on.dump");
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/SilencePlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/SilencePlaybackTest.java
index c1905da7c7..886b45aed6 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/SilencePlaybackTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/SilencePlaybackTest.java
@@ -21,11 +21,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
+import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
import com.google.android.exoplayer2.source.SilenceMediaSource;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@@ -36,6 +38,10 @@ import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
public final class SilencePlaybackTest {
+ @Rule
+ public ShadowMediaCodecConfig mediaCodecConfig =
+ ShadowMediaCodecConfig.forAllSupportedMimeTypes();
+
@Test
public void test_500ms() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WavPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WavPlaybackTest.java
new file mode 100644
index 0000000000..99418204b6
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/WavPlaybackTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.e2etest;
+
+import android.content.Context;
+import androidx.test.core.app.ApplicationProvider;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.robolectric.PlaybackOutput;
+import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
+import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
+import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
+import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
+import com.google.android.exoplayer2.testutil.DumpFileAsserts;
+import com.google.common.collect.ImmutableList;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** End-to-end tests using WAV samples. */
+// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
+@Config(sdk = 29)
+@RunWith(ParameterizedRobolectricTestRunner.class)
+public final class WavPlaybackTest {
+ @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+ public static ImmutableList mediaSamples() {
+ return ImmutableList.of("sample.wav", "sample_ima_adpcm.wav", "sample_with_trailing_bytes.wav");
+ }
+
+ @ParameterizedRobolectricTestRunner.Parameter public String inputFile;
+
+ @Rule
+ public ShadowMediaCodecConfig mediaCodecConfig =
+ ShadowMediaCodecConfig.forAllSupportedMimeTypes();
+
+ @Test
+ public void test() throws Exception {
+ Context applicationContext = ApplicationProvider.getApplicationContext();
+ CapturingRenderersFactory capturingRenderersFactory =
+ new CapturingRenderersFactory(applicationContext);
+ SimpleExoPlayer player =
+ new SimpleExoPlayer.Builder(applicationContext, capturingRenderersFactory)
+ .setClock(new AutoAdvancingFakeClock())
+ .build();
+ PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
+
+ player.setMediaItem(MediaItem.fromUri("asset:///media/wav/" + inputFile));
+ player.prepare();
+ player.play();
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
+ player.release();
+
+ DumpFileAsserts.assertOutput(
+ ApplicationProvider.getApplicationContext(),
+ playbackOutput,
+ "playbackdumps/wav/" + inputFile + ".dump");
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java
index 346aa95852..42dcaa572d 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/MetadataRendererTest.java
@@ -151,7 +151,7 @@ public class MetadataRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
EMSG_FORMAT,
ImmutableList.of(
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java
index 2007ecdb74..6700f0de7a 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java
@@ -38,8 +38,8 @@ import com.google.android.exoplayer2.testutil.FakeRenderer;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
@@ -99,8 +99,7 @@ public class DownloadHelperTest {
trackGroupTextZh);
TrackGroupArray trackGroupArraySingle =
new TrackGroupArray(TRACK_GROUP_VIDEO_SINGLE, trackGroupAudioUs);
- trackGroupArrays =
- new TrackGroupArray[] {trackGroupArrayAll, trackGroupArraySingle};
+ trackGroupArrays = new TrackGroupArray[] {trackGroupArrayAll, trackGroupArraySingle};
testMediaItem =
new MediaItem.Builder().setUri("http://test.uri").setCustomCacheKey("cacheKey").build();
@@ -194,17 +193,17 @@ public class DownloadHelperTest {
public void getTrackSelections_returnsInitialSelection() throws Exception {
prepareDownloadHelper(downloadHelper);
- List selectedText0 =
+ List selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
- List selectedAudio0 =
+ List selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
- List selectedVideo0 =
+ List selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
- List selectedText1 =
+ List selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
- List selectedAudio1 =
+ List selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
- List selectedVideo1 =
+ List selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertSingleTrackSelectionEquals(selectedText0, trackGroupTextUs, 0);
@@ -222,17 +221,17 @@ public class DownloadHelperTest {
// Clear only one period selection to verify second period selection is untouched.
downloadHelper.clearTrackSelections(/* periodIndex= */ 0);
- List selectedText0 =
+ List selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
- List selectedAudio0 =
+ List selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
- List selectedVideo0 =
+ List selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
- List selectedText1 =
+ List selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
- List selectedAudio1 =
+ List selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
- List selectedVideo1 =
+ List selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertThat(selectedText0).isEmpty();
@@ -258,17 +257,17 @@ public class DownloadHelperTest {
// Replace only one period selection to verify second period selection is untouched.
downloadHelper.replaceTrackSelections(/* periodIndex= */ 0, parameters);
- List selectedText0 =
+ List selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
- List selectedAudio0 =
+ List selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
- List selectedVideo0 =
+ List selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
- List selectedText1 =
+ List selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
- List selectedAudio1 =
+ List selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
- List selectedVideo1 =
+ List selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertSingleTrackSelectionEquals(selectedText0, trackGroupTextZh, 0);
@@ -294,17 +293,17 @@ public class DownloadHelperTest {
// Add only to one period selection to verify second period selection is untouched.
downloadHelper.addTrackSelection(/* periodIndex= */ 0, parameters);
- List selectedText0 =
+ List selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
- List selectedAudio0 =
+ List selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
- List selectedVideo0 =
+ List selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
- List selectedText1 =
+ List selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
- List selectedAudio1 =
+ List selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
- List selectedVideo1 =
+ List selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertSingleTrackSelectionEquals(selectedText0, trackGroupTextUs, 0);
@@ -327,17 +326,17 @@ public class DownloadHelperTest {
// Add a non-default language, and a non-existing language (which will select the default).
downloadHelper.addAudioLanguagesToSelection("ZH", "Klingonese");
- List selectedText0 =
+ List selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
- List selectedAudio0 =
+ List selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
- List selectedVideo0 =
+ List selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
- List selectedText1 =
+ List selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
- List selectedAudio1 =
+ List selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
- List selectedVideo1 =
+ List selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertThat(selectedVideo0).isEmpty();
@@ -361,17 +360,17 @@ public class DownloadHelperTest {
// Add a non-default language, and a non-existing language (which will select the default).
downloadHelper.addTextLanguagesToSelection(
/* selectUndeterminedTextLanguage= */ true, "ZH", "Klingonese");
- List selectedText0 =
+ List selectedText0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0);
- List selectedAudio0 =
+ List selectedAudio0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1);
- List selectedVideo0 =
+ List selectedVideo0 =
downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2);
- List selectedText1 =
+ List selectedText1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0);
- List selectedAudio1 =
+ List selectedAudio1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1);
- List selectedVideo1 =
+ List selectedVideo1 =
downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2);
assertThat(selectedVideo0).isEmpty();
@@ -464,13 +463,13 @@ public class DownloadHelperTest {
}
private static void assertSingleTrackSelectionEquals(
- List trackSelectionList, TrackGroup trackGroup, int... tracks) {
+ List trackSelectionList, TrackGroup trackGroup, int... tracks) {
assertThat(trackSelectionList).hasSize(1);
assertTrackSelectionEquals(trackSelectionList.get(0), trackGroup, tracks);
}
private static void assertTrackSelectionEquals(
- TrackSelection trackSelection, TrackGroup trackGroup, int... tracks) {
+ ExoTrackSelection trackSelection, TrackGroup trackGroup, int... tracks) {
assertThat(trackSelection.getTrackGroup()).isEqualTo(trackGroup);
assertThat(trackSelection.length()).isEqualTo(tracks.length);
int[] selectedTracksInGroup = new int[trackSelection.length()];
@@ -498,9 +497,9 @@ public class DownloadHelperTest {
new EventDispatcher()
.withParameters(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0)) {
@Override
- public List getStreamKeys(List trackSelections) {
+ public List getStreamKeys(List trackSelections) {
List result = new ArrayList<>();
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : trackSelections) {
int groupIndex = trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup());
for (int i = 0; i < trackSelection.length(); i++) {
result.add(
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MediaSourceDrmHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java
similarity index 75%
rename from library/core/src/test/java/com/google/android/exoplayer2/source/MediaSourceDrmHelperTest.java
rename to library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java
index 45384f05ec..4e597b6371 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/MediaSourceDrmHelperTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultDrmSessionManagerProviderTest.java
@@ -21,20 +21,21 @@ import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit tests for {@link MediaSourceDrmHelper}. */
+/** Unit tests for {@link DefaultDrmSessionManagerProvider}. */
@RunWith(AndroidJUnit4.class)
-public class MediaSourceDrmHelperTest {
+public class DefaultDrmSessionManagerProviderTest {
@Test
public void create_noDrmProperties_createsNoopManager() {
DrmSessionManager drmSessionManager =
- new MediaSourceDrmHelper().create(MediaItem.fromUri(Uri.EMPTY));
+ new DefaultDrmSessionManagerProvider().get(MediaItem.fromUri(Uri.EMPTY));
- assertThat(drmSessionManager).isEqualTo(DrmSessionManager.DUMMY);
+ assertThat(drmSessionManager).isEqualTo(DrmSessionManager.DRM_UNSUPPORTED);
}
@Test
@@ -46,8 +47,8 @@ public class MediaSourceDrmHelperTest {
.setDrmUuid(C.WIDEVINE_UUID)
.build();
- DrmSessionManager drmSessionManager = new MediaSourceDrmHelper().create(mediaItem);
+ DrmSessionManager drmSessionManager = new DefaultDrmSessionManagerProvider().get(mediaItem);
- assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DUMMY);
+ assertThat(drmSessionManager).isNotEqualTo(DrmSessionManager.DRM_UNSUPPORTED);
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java
index 26285d7e81..4a756ccf9f 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaPeriodTest.java
@@ -29,8 +29,8 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.testutil.FakeMediaPeriod;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.CountDownLatch;
@@ -72,13 +72,15 @@ public final class MergingMediaPeriodTest {
new MergingPeriodDefinition(
/* timeOffsetUs= */ 0, /* singleSampleTimeUs= */ 0, childFormat21, childFormat22));
- TrackSelection selectionForChild1 =
+ ExoTrackSelection selectionForChild1 =
new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(1), /* track= */ 0);
- TrackSelection selectionForChild2 =
+ ExoTrackSelection selectionForChild2 =
new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(2), /* track= */ 0);
SampleStream[] streams = new SampleStream[4];
mergingMediaPeriod.selectTracks(
- /* selections= */ new TrackSelection[] {null, selectionForChild1, selectionForChild2, null},
+ /* selections= */ new ExoTrackSelection[] {
+ null, selectionForChild1, selectionForChild2, null
+ },
/* mayRetainStreamFlags= */ new boolean[] {false, false, false, false},
streams,
/* streamResetFlags= */ new boolean[] {false, false, false, false},
@@ -117,13 +119,13 @@ public final class MergingMediaPeriodTest {
childFormat21,
childFormat22));
- TrackSelection selectionForChild1 =
+ ExoTrackSelection selectionForChild1 =
new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(0), /* track= */ 0);
- TrackSelection selectionForChild2 =
+ ExoTrackSelection selectionForChild2 =
new FixedTrackSelection(mergingMediaPeriod.getTrackGroups().get(2), /* track= */ 0);
SampleStream[] streams = new SampleStream[2];
mergingMediaPeriod.selectTracks(
- /* selections= */ new TrackSelection[] {selectionForChild1, selectionForChild2},
+ /* selections= */ new ExoTrackSelection[] {selectionForChild1, selectionForChild2},
/* mayRetainStreamFlags= */ new boolean[] {false, false},
streams,
/* streamResetFlags= */ new boolean[] {false, false},
@@ -210,7 +212,7 @@ public final class MergingMediaPeriodTest {
new DefaultAllocator(/* trimOnReset= */ false, /* individualAllocationSize= */ 1024),
trackDataFactory,
mediaSourceEventDispatcher,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* deferOnPrepared= */ false);
selectTracksPositionUs = C.TIME_UNSET;
@@ -218,7 +220,7 @@ public final class MergingMediaPeriodTest {
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java
index 90b29b30d5..aaf00388f6 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriodTest.java
@@ -49,7 +49,7 @@ public final class ProgressiveMediaPeriodTest {
Uri.parse("asset://android_asset/media/mp4/sample.mp4"),
new AssetDataSource(ApplicationProvider.getApplicationContext()),
() -> new Extractor[] {new Mp4Extractor()},
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
new DefaultLoadErrorHandlingPolicy(),
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
index 11a2204f81..db9eee2ba2 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -203,6 +203,22 @@ public final class SampleQueueTest {
assertNoSamplesToRead(null);
}
+ @Test
+ public void peekConsumesDownstreamFormat() {
+ sampleQueue.format(FORMAT_1);
+ clearFormatHolderAndInputBuffer();
+ int result =
+ sampleQueue.peek(
+ formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false);
+ assertThat(result).isEqualTo(RESULT_FORMAT_READ);
+ // formatHolder should be populated.
+ assertThat(formatHolder.format).isEqualTo(FORMAT_1);
+ result =
+ sampleQueue.peek(
+ formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false);
+ assertThat(result).isEqualTo(RESULT_NOTHING_READ);
+ }
+
@Test
public void equalFormatsDeduplicated() {
sampleQueue.format(FORMAT_1);
@@ -1625,10 +1641,32 @@ public final class SampleQueueTest {
byte[] sampleData,
int offset,
int length) {
+ // Check that peeks yields the expected values.
clearFormatHolderAndInputBuffer();
int result =
+ sampleQueue.peek(
+ formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false);
+ assertBufferReadResult(
+ result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted, sampleData, offset, length);
+
+ // Check that read yields the expected values.
+ clearFormatHolderAndInputBuffer();
+ result =
sampleQueue.read(
formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false);
+ assertBufferReadResult(
+ result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted, sampleData, offset, length);
+ }
+
+ private void assertBufferReadResult(
+ int result,
+ long timeUs,
+ boolean isKeyFrame,
+ boolean isDecodeOnly,
+ boolean isEncrypted,
+ byte[] sampleData,
+ int offset,
+ int length) {
assertThat(result).isEqualTo(RESULT_BUFFER_READ);
// formatHolder should not be populated.
assertThat(formatHolder.format).isNull();
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java
index c7833fab04..a734019d09 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java
@@ -18,10 +18,13 @@ package com.google.android.exoplayer2.text.ssa;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import android.graphics.Color;
import android.text.Layout;
+import android.text.Spanned;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.testutil.truth.SpannedSubject;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.common.collect.Iterables;
@@ -44,6 +47,7 @@ public final class SsaDecoderTest {
private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
private static final String INVALID_POSITIONS = "media/ssa/invalid_positioning";
private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres";
+ private static final String COLORS = "media/ssa/colors";
@Test
public void decodeEmpty() throws IOException {
@@ -267,6 +271,54 @@ public final class SsaDecoderTest {
assertTypicalCue3(subtitle, 0);
}
+ @Test
+ public void decodeColors() throws IOException {
+ SsaDecoder decoder = new SsaDecoder();
+ byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), COLORS);
+ Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
+ assertThat(subtitle.getEventTimeCount()).isEqualTo(14);
+ // &H000000FF (AABBGGRR) -> #FFFF0000 (AARRGGBB)
+ Spanned firstCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text;
+ SpannedSubject.assertThat(firstCueText)
+ .hasForegroundColorSpanBetween(0, firstCueText.length())
+ .withColor(Color.RED);
+ // &H0000FFFF (AABBGGRR) -> #FFFFFF00 (AARRGGBB)
+ Spanned secondCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text;
+ SpannedSubject.assertThat(secondCueText)
+ .hasForegroundColorSpanBetween(0, secondCueText.length())
+ .withColor(Color.YELLOW);
+ // &HFF00 (GGRR) -> #FF00FF00 (AARRGGBB)
+ Spanned thirdCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))).text;
+ SpannedSubject.assertThat(thirdCueText)
+ .hasForegroundColorSpanBetween(0, thirdCueText.length())
+ .withColor(Color.GREEN);
+ // &HA00000FF (AABBGGRR) -> #5FFF0000 (AARRGGBB)
+ Spanned fourthCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))).text;
+ SpannedSubject.assertThat(fourthCueText)
+ .hasForegroundColorSpanBetween(0, fourthCueText.length())
+ .withColor(0x5FFF0000);
+ // 16711680 (AABBGGRR) -> &H00FF0000 (AABBGGRR) -> #FF0000FF (AARRGGBB)
+ Spanned fifthCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))).text;
+ SpannedSubject.assertThat(fifthCueText)
+ .hasForegroundColorSpanBetween(0, fifthCueText.length())
+ .withColor(0xFF0000FF);
+ // 2164195328 (AABBGGRR) -> &H80FF0000 (AABBGGRR) -> #7F0000FF (AARRGGBB)
+ Spanned sixthCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))).text;
+ SpannedSubject.assertThat(sixthCueText)
+ .hasForegroundColorSpanBetween(0, sixthCueText.length())
+ .withColor(0x7F0000FF);
+ Spanned seventhCueText =
+ (Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12))).text;
+ SpannedSubject.assertThat(seventhCueText)
+ .hasNoForegroundColorSpanBetween(0, seventhCueText.length());
+ }
+
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
index 4de6297a94..aa6f420c3e 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
@@ -30,7 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaChunk;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection.AdaptationCheckpoint;
-import com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection.Definition;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
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 e1ff3002eb..9aebfb7718 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
@@ -323,7 +323,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, formatWithSelectionFlag);
+ assertFixedSelection(result.selections[0], trackGroups, formatWithSelectionFlag);
}
/** Tests that adaptive audio track selections respect the maximum audio bitrate. */
@@ -341,25 +341,25 @@ public final class DefaultTrackSelectorTest {
TrackSelectorResult result =
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 2, 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 2, 0, 1);
trackSelector.setParameters(
trackSelector.buildUponParameters().setMaxAudioBitrate(256 * 1024 - 1));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
trackSelector.setParameters(
trackSelector.buildUponParameters().setMaxAudioBitrate(192 * 1024 - 1));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups.get(0), 1);
+ assertFixedSelection(result.selections[0], trackGroups.get(0), 1);
trackSelector.setParameters(trackSelector.buildUponParameters().setMaxAudioBitrate(10));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups.get(0), 1);
+ assertFixedSelection(result.selections[0], trackGroups.get(0), 1);
}
/**
@@ -380,7 +380,7 @@ public final class DefaultTrackSelectorTest {
wrapFormats(frAudioFormat, enAudioFormat),
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, enAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, enAudioFormat);
}
/**
@@ -408,7 +408,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, moreRoleFlags);
+ assertFixedSelection(result.selections[0], trackGroups, moreRoleFlags);
}
/**
@@ -429,7 +429,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, defaultFormat);
+ assertFixedSelection(result.selections[0], trackGroups, defaultFormat);
}
/**
@@ -449,7 +449,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, firstFormat);
+ assertFixedSelection(result.selections[0], trackGroups, firstFormat);
}
/**
@@ -471,7 +471,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, enNonDefaultFormat);
+ assertFixedSelection(result.selections[0], trackGroups, enNonDefaultFormat);
}
/**
@@ -497,7 +497,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, supportedFormat);
+ assertFixedSelection(result.selections[0], trackGroups, supportedFormat);
}
/**
@@ -515,7 +515,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, AUDIO_FORMAT);
+ assertFixedSelection(result.selections[0], trackGroups, AUDIO_FORMAT);
}
/**
@@ -536,7 +536,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
}
/**
@@ -563,7 +563,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, supportedFormat);
+ assertFixedSelection(result.selections[0], trackGroups, supportedFormat);
}
/**
@@ -591,7 +591,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, supportedFrFormat);
+ assertFixedSelection(result.selections[0], trackGroups, supportedFrFormat);
}
/**
@@ -626,7 +626,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, supportedFrFormat);
+ assertFixedSelection(result.selections[0], trackGroups, supportedFrFormat);
}
/**
@@ -646,7 +646,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, higherChannelFormat);
+ assertFixedSelection(result.selections[0], trackGroups, higherChannelFormat);
}
/**
@@ -666,7 +666,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, higherSampleRateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, higherSampleRateFormat);
}
/**
@@ -687,7 +687,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, higherBitrateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, higherBitrateFormat);
}
/**
@@ -709,7 +709,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, firstLanguageFormat);
+ assertFixedSelection(result.selections[0], trackGroups, firstLanguageFormat);
}
/**
@@ -733,7 +733,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, higherChannelLowerSampleRateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, higherChannelLowerSampleRateFormat);
}
/**
@@ -756,7 +756,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, higherSampleRateLowerBitrateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, higherSampleRateLowerBitrateFormat);
}
/**
@@ -776,7 +776,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, lowerChannelFormat);
+ assertFixedSelection(result.selections[0], trackGroups, lowerChannelFormat);
}
/**
@@ -796,7 +796,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, lowerSampleRateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, lowerSampleRateFormat);
}
/**
@@ -816,7 +816,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, lowerBitrateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, lowerBitrateFormat);
}
/**
@@ -841,7 +841,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, lowerChannelHigherSampleRateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, lowerChannelHigherSampleRateFormat);
}
/**
@@ -865,7 +865,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, lowerSampleRateHigherBitrateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, lowerSampleRateHigherBitrateFormat);
}
/** Tests text track selection flags. */
@@ -885,12 +885,12 @@ public final class DefaultTrackSelectorTest {
TrackGroupArray trackGroups = wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag);
TrackSelectorResult result =
trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, forcedDefault);
+ assertFixedSelection(result.selections[0], trackGroups, forcedDefault);
// Ditto.
trackGroups = wrapFormats(forcedOnly, noFlag, defaultOnly);
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly);
+ assertFixedSelection(result.selections[0], trackGroups, defaultOnly);
// Default flags are disabled and no language preference is provided, so no text track is
// selected.
@@ -898,7 +898,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.setParameters(
defaultParameters.buildUpon().setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
// All selection flags are disabled and there is no language preference, so nothing should be
// selected.
@@ -910,13 +910,13 @@ public final class DefaultTrackSelectorTest {
.setDisabledTextTrackSelectionFlags(
C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
// There is a preferred language, so a language-matching track flagged as default should
// be selected, and the one without forced flag should be preferred.
trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("eng"));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly);
+ assertFixedSelection(result.selections[0], trackGroups, defaultOnly);
// Same as above, but the default flag is disabled. If multiple tracks match the preferred
// language, those not flagged as forced are preferred, as they likely include the contents of
@@ -928,7 +928,7 @@ public final class DefaultTrackSelectorTest {
.buildUpon()
.setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, noFlag);
+ assertFixedSelection(result.selections[0], trackGroups, noFlag);
}
/**
@@ -957,23 +957,23 @@ public final class DefaultTrackSelectorTest {
TrackGroupArray trackGroups = wrapFormats(noLanguageAudio, forcedNoLanguage);
TrackSelectorResult result =
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(1), trackGroups, forcedNoLanguage);
+ assertFixedSelection(result.selections[1], trackGroups, forcedNoLanguage);
// No forced text track should be selected because none of the forced text tracks' languages
// matches the selected audio language.
trackGroups = wrapFormats(noLanguageAudio, forcedEnglish, forcedGerman);
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(1));
+ assertNoSelection(result.selections[1]);
// The audio declares german. The german forced track should be selected.
trackGroups = wrapFormats(germanAudio, forcedGerman, forcedEnglish);
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(1), trackGroups, forcedGerman);
+ assertFixedSelection(result.selections[1], trackGroups, forcedGerman);
// Ditto
trackGroups = wrapFormats(germanAudio, forcedEnglish, forcedGerman);
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(1), trackGroups, forcedGerman);
+ assertFixedSelection(result.selections[1], trackGroups, forcedGerman);
}
/**
@@ -995,34 +995,34 @@ public final class DefaultTrackSelectorTest {
TrackGroupArray trackGroups = wrapFormats(spanish, german, undeterminedUnd, undeterminedNull);
TrackSelectorResult result =
trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
trackSelector.setParameters(
defaultParameters.buildUpon().setSelectUndeterminedTextLanguage(true));
result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd);
+ assertFixedSelection(result.selections[0], trackGroups, undeterminedUnd);
ParametersBuilder builder = defaultParameters.buildUpon().setPreferredTextLanguage("spa");
trackSelector.setParameters(builder);
result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, spanish);
+ assertFixedSelection(result.selections[0], trackGroups, spanish);
trackGroups = wrapFormats(german, undeterminedUnd, undeterminedNull);
result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true));
result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, undeterminedUnd);
+ assertFixedSelection(result.selections[0], trackGroups, undeterminedUnd);
trackGroups = wrapFormats(german, undeterminedNull);
result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, undeterminedNull);
+ assertFixedSelection(result.selections[0], trackGroups, undeterminedNull);
trackGroups = wrapFormats(german);
result = trackSelector.selectTracks(textRendererCapabilites, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
}
/** Tests audio track selection when there are multiple audio renderers. */
@@ -1053,20 +1053,20 @@ public final class DefaultTrackSelectorTest {
// Without an explicit language preference, nothing should be selected.
TrackSelectorResult result =
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
- assertNoSelection(result.selections.get(1));
+ assertNoSelection(result.selections[0]);
+ assertNoSelection(result.selections[1]);
// Explicit language preference for english. First renderer should be used.
trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("en"));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, english);
- assertNoSelection(result.selections.get(1));
+ assertFixedSelection(result.selections[0], trackGroups, english);
+ assertNoSelection(result.selections[1]);
// Explicit language preference for German. Second renderer should be used.
trackSelector.setParameters(defaultParameters.buildUpon().setPreferredTextLanguage("de"));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
- assertFixedSelection(result.selections.get(1), trackGroups, german);
+ assertNoSelection(result.selections[0]);
+ assertFixedSelection(result.selections[1], trackGroups, german);
}
/**
@@ -1098,7 +1098,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, lowerBitrateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, lowerBitrateFormat);
}
/**
@@ -1130,7 +1130,7 @@ public final class DefaultTrackSelectorTest {
trackGroups,
periodId,
TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, higherBitrateFormat);
+ assertFixedSelection(result.selections[0], trackGroups, higherBitrateFormat);
}
@Test
@@ -1143,7 +1143,7 @@ public final class DefaultTrackSelectorTest {
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
}
@Test
@@ -1174,7 +1174,7 @@ public final class DefaultTrackSelectorTest {
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 6);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 6);
}
@Test
@@ -1189,7 +1189,7 @@ public final class DefaultTrackSelectorTest {
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups.get(0), /* expectedTrack= */ 0);
+ assertFixedSelection(result.selections[0], trackGroups.get(0), /* expectedTrack= */ 0);
}
@Test
@@ -1206,7 +1206,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, highSampleRateAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, highSampleRateAudioFormat);
// The same applies if the tracks are provided in the opposite order.
trackGroups = singleTrackGroup(lowSampleRateAudioFormat, highSampleRateAudioFormat);
@@ -1214,7 +1214,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, highSampleRateAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, highSampleRateAudioFormat);
// If we explicitly enable mixed sample rate adaptiveness, expect an adaptive selection.
trackSelector.setParameters(
@@ -1223,7 +1223,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
}
@Test
@@ -1239,7 +1239,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, aacAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, aacAudioFormat);
// The same applies if the tracks are provided in the opposite order.
trackGroups = singleTrackGroup(opusAudioFormat, aacAudioFormat);
@@ -1247,7 +1247,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, opusAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, opusAudioFormat);
// If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection.
trackSelector.setParameters(
@@ -1256,7 +1256,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
}
@Test
@@ -1272,7 +1272,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, surroundAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, surroundAudioFormat);
// The same applies if the tracks are provided in the opposite order.
trackGroups = singleTrackGroup(surroundAudioFormat, stereoAudioFormat);
@@ -1280,7 +1280,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, surroundAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, surroundAudioFormat);
// If we constrain the channel count to 4 we expect a fixed selection containing the track with
// fewer channels.
@@ -1289,7 +1289,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, stereoAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, stereoAudioFormat);
// If we constrain the channel count to 2 we expect a fixed selection containing the track with
// fewer channels.
@@ -1298,7 +1298,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, stereoAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, stereoAudioFormat);
// If we constrain the channel count to 1 we expect a fixed selection containing the track with
// fewer channels.
@@ -1307,7 +1307,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, stereoAudioFormat);
+ assertFixedSelection(result.selections[0], trackGroups, stereoAudioFormat);
// If we disable exceeding of constraints we expect no selection.
trackSelector.setParameters(
@@ -1319,7 +1319,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertNoSelection(result.selections.get(0));
+ assertNoSelection(result.selections[0]);
}
@Test
@@ -1343,7 +1343,7 @@ public final class DefaultTrackSelectorTest {
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1, 2);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 1, 2);
}
/** Tests audio track selection when there are multiple audio renderers. */
@@ -1374,20 +1374,20 @@ public final class DefaultTrackSelectorTest {
TrackGroupArray trackGroups = wrapFormats(english, german);
TrackSelectorResult result =
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, english);
- assertNoSelection(result.selections.get(1));
+ assertFixedSelection(result.selections[0], trackGroups, english);
+ assertNoSelection(result.selections[1]);
// Explicit language preference for english. First renderer should be used.
trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("en"));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertFixedSelection(result.selections.get(0), trackGroups, english);
- assertNoSelection(result.selections.get(1));
+ assertFixedSelection(result.selections[0], trackGroups, english);
+ assertNoSelection(result.selections[1]);
// Explicit language preference for German. Second renderer should be used.
trackSelector.setParameters(defaultParameters.buildUpon().setPreferredAudioLanguage("de"));
result = trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
- assertNoSelection(result.selections.get(0));
- assertFixedSelection(result.selections.get(1), trackGroups, german);
+ assertNoSelection(result.selections[0]);
+ assertFixedSelection(result.selections[1], trackGroups, german);
}
@Test
@@ -1400,7 +1400,7 @@ public final class DefaultTrackSelectorTest {
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
}
@Test
@@ -1424,7 +1424,7 @@ public final class DefaultTrackSelectorTest {
periodId,
TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
// If we explicitly disable non-seamless adaptiveness, expect a fixed selection.
trackSelector.setParameters(
@@ -1436,7 +1436,7 @@ public final class DefaultTrackSelectorTest {
periodId,
TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups.get(0), 0);
+ assertFixedSelection(result.selections[0], trackGroups.get(0), 0);
}
@Test
@@ -1452,7 +1452,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, h264VideoFormat);
+ assertFixedSelection(result.selections[0], trackGroups, h264VideoFormat);
// The same applies if the tracks are provided in the opposite order.
trackGroups = singleTrackGroup(h265VideoFormat, h264VideoFormat);
@@ -1460,7 +1460,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, h265VideoFormat);
+ assertFixedSelection(result.selections[0], trackGroups, h265VideoFormat);
// If we explicitly enable mixed mime type adaptiveness, expect an adaptive selection.
trackSelector.setParameters(
@@ -1469,7 +1469,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 0, 1);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1);
}
@Test
@@ -1493,7 +1493,7 @@ public final class DefaultTrackSelectorTest {
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertAdaptiveSelection(result.selections.get(0), trackGroups.get(0), 1, 2);
+ assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 1, 2);
}
@Test
@@ -1518,9 +1518,9 @@ public final class DefaultTrackSelectorTest {
assertThat(result.length).isEqualTo(2);
assertAdaptiveSelection(
- result.selections.get(0), trackGroups.get(0), /* expectedTracks...= */ 1, 0);
+ result.selections[0], trackGroups.get(0), /* expectedTracks...= */ 1, 0);
assertAdaptiveSelection(
- result.selections.get(1), trackGroups.get(1), /* expectedTracks...= */ 1, 0);
+ result.selections[1], trackGroups.get(1), /* expectedTracks...= */ 1, 0);
// Multiple adaptive selection disallowed.
trackSelector.setParameters(
@@ -1534,8 +1534,8 @@ public final class DefaultTrackSelectorTest {
assertThat(result.length).isEqualTo(2);
assertAdaptiveSelection(
- result.selections.get(0), trackGroups.get(0), /* expectedTracks...= */ 1, 0);
- assertFixedSelection(result.selections.get(1), trackGroups.get(1), /* expectedTrack= */ 1);
+ result.selections[0], trackGroups.get(0), /* expectedTracks...= */ 1, 0);
+ assertFixedSelection(result.selections[1], trackGroups.get(1), /* expectedTrack= */ 1);
}
@Test
@@ -1552,7 +1552,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatVp9);
+ assertFixedSelection(result.selections[0], trackGroups, formatVp9);
trackSelector.setParameters(
trackSelector
@@ -1562,7 +1562,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatVp9);
+ assertFixedSelection(result.selections[0], trackGroups, formatVp9);
trackSelector.setParameters(
trackSelector
@@ -1572,7 +1572,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatH264);
+ assertFixedSelection(result.selections[0], trackGroups, formatH264);
// Select first in the list if no preference is specified.
trackSelector.setParameters(
@@ -1581,7 +1581,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {VIDEO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatAv1);
+ assertFixedSelection(result.selections[0], trackGroups, formatAv1);
}
@Test
@@ -1598,7 +1598,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatAc4);
+ assertFixedSelection(result.selections[0], trackGroups, formatAc4);
trackSelector.setParameters(
trackSelector
@@ -1608,7 +1608,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatAc4);
+ assertFixedSelection(result.selections[0], trackGroups, formatAc4);
trackSelector.setParameters(
trackSelector
@@ -1618,7 +1618,7 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatEAc3);
+ assertFixedSelection(result.selections[0], trackGroups, formatEAc3);
// Select first in the list if no preference is specified.
trackSelector.setParameters(
@@ -1627,13 +1627,13 @@ public final class DefaultTrackSelectorTest {
trackSelector.selectTracks(
new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE);
assertThat(result.length).isEqualTo(1);
- assertFixedSelection(result.selections.get(0), trackGroups, formatAac);
+ assertFixedSelection(result.selections[0], trackGroups, formatAac);
}
private static void assertSelections(TrackSelectorResult result, TrackSelection[] expected) {
assertThat(result.length).isEqualTo(expected.length);
for (int i = 0; i < expected.length; i++) {
- assertThat(result.selections.get(i)).isEqualTo(expected[i]);
+ assertThat(result.selections[i]).isEqualTo(expected[i]);
}
}
@@ -1771,11 +1771,11 @@ public final class DefaultTrackSelectorTest {
@Capabilities private final int supportValue;
/**
- * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all
- * tracks of the given type.
+ * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all tracks of
+ * the given type.
*
* @param trackType the track type of all formats that this renderer capabilities advertises
- * support for.
+ * support for.
*/
FakeRendererCapabilities(int trackType) {
this(
@@ -1820,7 +1820,6 @@ public final class DefaultTrackSelectorTest {
public int supportsMixedMimeTypeAdaptation() {
return ADAPTIVE_SEAMLESS;
}
-
}
/**
@@ -1869,7 +1868,5 @@ public final class DefaultTrackSelectorTest {
public int supportsMixedMimeTypeAdaptation() {
return ADAPTIVE_SEAMLESS;
}
-
}
-
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java
index f86428a950..9a01f85aa9 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java
@@ -138,7 +138,7 @@ public final class MappingTrackSelectorTest {
private MappedTrackInfo lastMappedTrackInfo;
@Override
- protected Pair selectTracks(
+ protected Pair selectTracks(
MappedTrackInfo mappedTrackInfo,
@Capabilities int[][][] rendererFormatSupports,
@AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,
@@ -147,7 +147,7 @@ public final class MappingTrackSelectorTest {
int rendererCount = mappedTrackInfo.getRendererCount();
lastMappedTrackInfo = mappedTrackInfo;
return Pair.create(
- new RendererConfiguration[rendererCount], new TrackSelection[rendererCount]);
+ new RendererConfiguration[rendererCount], new ExoTrackSelection[rendererCount]);
}
public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) {
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceContractTest.java
new file mode 100644
index 0000000000..9df4155616
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceContractTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link AssetDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class AssetDataSourceContractTest extends DataSourceContractTest {
+
+ // We pick an arbitrary file from the assets. The selected file has a convenient size of 1024
+ // bytes.
+ private static final String ASSET_PATH = "media/mp3/1024_incrementing_bytes.mp3";
+ private static final Uri ASSET_URI = Uri.parse("asset:///" + ASSET_PATH);
+
+ private byte[] data;
+
+ @Before
+ public void setUp() throws IOException {
+ data = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), ASSET_PATH);
+ }
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return ImmutableList.of(
+ new TestResource.Builder()
+ .setName("simple")
+ .setUri(ASSET_URI)
+ .setExpectedBytes(data)
+ .build());
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.parse("asset:///nonexistentdir/nonexistentfile");
+ }
+
+ @Override
+ protected DataSource createDataSource() {
+ return new AssetDataSource(ApplicationProvider.getApplicationContext());
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceContractTest.java
new file mode 100644
index 0000000000..1aa2198fc5
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceContractTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.common.collect.ImmutableList;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link ByteArrayDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class ByteArrayDataSourceContractTest extends DataSourceContractTest {
+
+ private static final byte[] DATA = TestUtil.buildTestData(20);
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return ImmutableList.of(
+ new TestResource.Builder()
+ .setName("simple")
+ .setUri(Uri.EMPTY)
+ .setExpectedBytes(DATA)
+ .build());
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected DataSource createDataSource() {
+ return new ByteArrayDataSource(DATA);
+ }
+
+ @Override
+ @Test
+ @Ignore
+ public void resourceNotFound() {}
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java
new file mode 100644
index 0000000000..b75ff45f13
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/CacheDataSourceContractTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
+import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
+import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link CacheDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class CacheDataSourceContractTest extends DataSourceContractTest {
+ private static final byte[] DATA = TestUtil.buildTestData(20);
+
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private Uri simpleUri;
+
+ @Before
+ public void setUp() throws IOException {
+ File file = tempFolder.newFile();
+ Files.write(Paths.get(file.getAbsolutePath()), DATA);
+ simpleUri = Uri.fromFile(file);
+ }
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return ImmutableList.of(
+ new TestResource.Builder()
+ .setName("simple")
+ .setUri(simpleUri)
+ .setExpectedBytes(DATA)
+ .build());
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.fromFile(tempFolder.getRoot().toPath().resolve("nonexistent").toFile());
+ }
+
+ @Override
+ protected DataSource createDataSource() throws IOException {
+ File tempFolder =
+ Util.createTempDirectory(ApplicationProvider.getApplicationContext(), "ExoPlayerTest");
+ SimpleCache cache =
+ new SimpleCache(tempFolder, new NoOpCacheEvictor(), TestUtil.getInMemoryDatabaseProvider());
+ return new CacheDataSource(cache, new FileDataSource());
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java
new file mode 100644
index 0000000000..97bd701865
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceContractTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.net.Uri;
+import android.util.Base64;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.testutil.DataSourceContractTest;
+import com.google.android.exoplayer2.testutil.TestUtil;
+import com.google.common.collect.ImmutableList;
+import java.util.Random;
+import org.junit.runner.RunWith;
+
+/** {@link DataSource} contract tests for {@link ByteArrayDataSource}. */
+@RunWith(AndroidJUnit4.class)
+public class DataSchemeDataSourceContractTest extends DataSourceContractTest {
+
+ private static final String DATA = TestUtil.buildTestString(20, new Random(0));
+ private static final String BASE64_ENCODED_DATA =
+ Base64.encodeToString(TestUtil.buildTestData(20), Base64.DEFAULT);
+
+ @Override
+ protected ImmutableList getTestResources() {
+ return ImmutableList.of(
+ new TestResource.Builder()
+ .setName("plain text")
+ .setUri(Uri.parse("data:text/plain," + DATA))
+ .setExpectedBytes(DATA.getBytes(UTF_8))
+ .build(),
+ new TestResource.Builder()
+ .setName("base64 encoded text")
+ .setUri(Uri.parse("data:text/plain;base64," + BASE64_ENCODED_DATA))
+ .setExpectedBytes(Base64.decode(BASE64_ENCODED_DATA, Base64.DEFAULT))
+ .build());
+ }
+
+ @Override
+ protected Uri getNotFoundUri() {
+ return Uri.parse("data:");
+ }
+
+ @Override
+ protected DataSource createDataSource() {
+ return new DataSchemeDataSource();
+ }
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java
index d09c2e0b3d..848b0ce410 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/video/DecoderVideoRendererTest.java
@@ -187,7 +187,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -218,7 +218,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -248,7 +248,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -281,7 +281,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(
@@ -291,7 +291,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(oneByteSample(/* timeUs= */ 0), END_OF_STREAM_ITEM));
@@ -333,7 +333,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(
@@ -343,7 +343,7 @@ public final class DecoderVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ H264_FORMAT,
ImmutableList.of(oneByteSample(/* timeUs= */ 0), END_OF_STREAM_ITEM));
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java
index c0ec86d959..ccc4e89d58 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java
@@ -128,7 +128,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(
@@ -167,7 +167,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(
@@ -212,7 +212,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ pAsp1,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -264,7 +264,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -303,7 +303,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -333,7 +333,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -362,7 +362,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME)));
@@ -393,7 +393,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(
@@ -403,7 +403,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(
@@ -447,7 +447,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(
@@ -457,7 +457,7 @@ public class MediaCodecVideoRendererTest {
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
- DrmSessionManager.DUMMY,
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
/* initialFormat= */ VIDEO_H264,
ImmutableList.of(
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
index e12a67a754..d93915f761 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java
@@ -21,14 +21,12 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.chunk.ChunkSource;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.util.List;
-/**
- * An {@link ChunkSource} for DASH streams.
- */
+/** A {@link ChunkSource} for DASH streams. */
public interface DashChunkSource extends ChunkSource {
/** Factory for {@link DashChunkSource}s. */
@@ -55,7 +53,7 @@ public interface DashChunkSource extends ChunkSource {
DashManifest manifest,
int periodIndex,
int[] adaptationSetIndices,
- TrackSelection trackSelection,
+ ExoTrackSelection trackSelection,
int type,
long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack,
@@ -76,5 +74,5 @@ public interface DashChunkSource extends ChunkSource {
*
* @param trackSelection The new track selection instance. Must be equivalent to the previous one.
*/
- void updateTrackSelection(TrackSelection trackSelection);
+ void updateTrackSelection(ExoTrackSelection trackSelection);
}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
index 81d72b61f3..6cf10b3578 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
@@ -47,7 +47,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import com.google.android.exoplayer2.source.dash.manifest.EventStream;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
@@ -213,10 +213,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
- public List getStreamKeys(List trackSelections) {
+ public List getStreamKeys(List trackSelections) {
List manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
List streamKeys = new ArrayList<>();
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : trackSelections) {
int trackGroupIndex = trackGroups.indexOf(trackSelection.getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory != TrackGroupInfo.CATEGORY_PRIMARY) {
@@ -256,7 +256,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -356,7 +356,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// Internal methods.
- private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) {
+ private int[] getStreamIndexToTrackGroupIndex(ExoTrackSelection[] selections) {
int[] streamIndexToTrackGroupIndex = new int[selections.length];
for (int i = 0; i < selections.length; i++) {
if (selections[i] != null) {
@@ -369,7 +369,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
private void releaseDisabledStreams(
- TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) {
+ ExoTrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) {
for (int i = 0; i < selections.length; i++) {
if (selections[i] == null || !mayRetainStreamFlags[i]) {
if (streams[i] instanceof ChunkSampleStream) {
@@ -386,7 +386,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
private void releaseOrphanEmbeddedStreams(
- TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) {
+ ExoTrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) {
// We need to release an embedded stream if the corresponding primary stream is released.
@@ -414,14 +414,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
private void selectNewStreams(
- TrackSelection[] selections,
+ ExoTrackSelection[] selections,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs,
int[] streamIndexToTrackGroupIndex) {
// Create newly selected primary and event streams.
for (int i = 0; i < selections.length; i++) {
- TrackSelection selection = selections[i];
+ ExoTrackSelection selection = selections[i];
if (selection == null) {
continue;
}
@@ -703,8 +703,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return trackGroupCount;
}
- private static void buildManifestEventTrackGroupInfos(List eventStreams,
- TrackGroup[] trackGroups, TrackGroupInfo[] trackGroupInfos, int existingTrackGroupCount) {
+ private static void buildManifestEventTrackGroupInfos(
+ List eventStreams,
+ TrackGroup[] trackGroups,
+ TrackGroupInfo[] trackGroupInfos,
+ int existingTrackGroupCount) {
for (int i = 0; i < eventStreams.size(); i++) {
EventStream eventStream = eventStreams.get(i);
Format format =
@@ -717,8 +720,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
}
- private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo,
- TrackSelection selection, long positionUs) {
+ private ChunkSampleStream buildSampleStream(
+ TrackGroupInfo trackGroupInfo, ExoTrackSelection selection, long positionUs) {
int embeddedTrackCount = 0;
boolean enableEventMessageTrack =
trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET;
@@ -813,8 +816,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return null;
}
- private static boolean hasEventMessageTrack(List adaptationSets,
- int[] adaptationSetIndices) {
+ private static boolean hasEventMessageTrack(
+ List adaptationSets, int[] adaptationSetIndices) {
for (int i : adaptationSetIndices) {
List representations = adaptationSets.get(i).representations;
for (int j = 0; j < representations.size(); j++) {
@@ -897,8 +900,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public @interface TrackGroupCategory {}
/**
- * A normal track group that has its samples drawn from the stream.
- * For example: a video Track Group or an audio Track Group.
+ * A normal track group that has its samples drawn from the stream. For example: a video Track
+ * Group or an audio Track Group.
*/
private static final int CATEGORY_PRIMARY = 0;
@@ -909,9 +912,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final int CATEGORY_EMBEDDED = 1;
/**
- * A track group that has its samples listed explicitly in the DASH manifest file.
- * For example: an EventStream track has its sample (Events) included directly in the DASH
- * manifest file.
+ * A track group that has its samples listed explicitly in the DASH manifest file. For example:
+ * an EventStream track has its sample (Events) included directly in the DASH manifest file.
*/
private static final int CATEGORY_MANIFEST_EVENTS = 2;
@@ -940,8 +942,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* eventStreamGroupIndex= */ -1);
}
- public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices,
- int primaryTrackGroupIndex) {
+ public static TrackGroupInfo embeddedEmsgTrack(
+ int[] adaptationSetIndices, int primaryTrackGroupIndex) {
return new TrackGroupInfo(
C.TRACK_TYPE_METADATA,
CATEGORY_EMBEDDED,
@@ -992,5 +994,4 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.eventStreamGroupIndex = eventStreamGroupIndex;
}
}
-
}
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 fb576fcd63..258ebf3270 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
@@ -33,8 +33,10 @@ import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource;
@@ -44,7 +46,6 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaSourceFactory;
@@ -99,10 +100,10 @@ public final class DashMediaSource extends BaseMediaSource {
public static final class Factory implements MediaSourceFactory {
private final DashChunkSource.Factory chunkSourceFactory;
- private final MediaSourceDrmHelper mediaSourceDrmHelper;
@Nullable private final DataSource.Factory manifestDataSourceFactory;
- @Nullable private DrmSessionManager drmSessionManager;
+ private boolean usingCustomDrmSessionManagerProvider;
+ private DrmSessionManagerProvider drmSessionManagerProvider;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long targetLiveOffsetOverrideMs;
@@ -135,7 +136,7 @@ public final class DashMediaSource extends BaseMediaSource {
@Nullable DataSource.Factory manifestDataSourceFactory) {
this.chunkSourceFactory = checkNotNull(chunkSourceFactory);
this.manifestDataSourceFactory = manifestDataSourceFactory;
- mediaSourceDrmHelper = new MediaSourceDrmHelper();
+ drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
targetLiveOffsetOverrideMs = C.TIME_UNSET;
fallbackTargetLiveOffsetMs = DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS;
@@ -165,22 +166,44 @@ public final class DashMediaSource extends BaseMediaSource {
return this;
}
+ @Override
+ public Factory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ if (drmSessionManagerProvider != null) {
+ this.drmSessionManagerProvider = drmSessionManagerProvider;
+ this.usingCustomDrmSessionManagerProvider = true;
+ } else {
+ this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
+ this.usingCustomDrmSessionManagerProvider = false;
+ }
+ return this;
+ }
+
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
- this.drmSessionManager = drmSessionManager;
+ if (drmSessionManager == null) {
+ setDrmSessionManagerProvider(null);
+ } else {
+ setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
+ }
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
- mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider)
+ .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ }
return this;
}
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
- mediaSourceDrmHelper.setDrmUserAgent(userAgent);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent);
+ }
return this;
}
@@ -272,7 +295,7 @@ public final class DashMediaSource extends BaseMediaSource {
manifest,
new MediaItem.Builder()
.setUri(Uri.EMPTY)
- .setMediaId(DUMMY_MEDIA_ID)
+ .setMediaId(DEFAULT_MEDIA_ID)
.setMimeType(MimeTypes.APPLICATION_MPD)
.setStreamKeys(streamKeys)
.setTag(tag)
@@ -319,7 +342,7 @@ public final class DashMediaSource extends BaseMediaSource {
/* manifestParser= */ null,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
+ drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
fallbackTargetLiveOffsetMs);
}
@@ -385,7 +408,7 @@ public final class DashMediaSource extends BaseMediaSource {
manifestParser,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
+ drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
fallbackTargetLiveOffsetMs);
}
@@ -404,8 +427,7 @@ public final class DashMediaSource extends BaseMediaSource {
/** @deprecated Use {@link #DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS} instead. */
@Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30_000;
/** The media id used by media items of dash media sources without a manifest URI. */
- public static final String DUMMY_MEDIA_ID =
- "com.google.android.exoplayer2.source.dash.DashMediaSource";
+ public static final String DEFAULT_MEDIA_ID = "DashMediaSource";
/**
* The interval in milliseconds between invocations of {@link
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 9d1ec5a23f..2225589950 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,7 +47,7 @@ import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
@@ -84,7 +84,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
DashManifest manifest,
int periodIndex,
int[] adaptationSetIndices,
- TrackSelection trackSelection,
+ ExoTrackSelection trackSelection,
int trackType,
long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack,
@@ -122,7 +122,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
protected final RepresentationHolder[] representationHolders;
- private TrackSelection trackSelection;
+ private ExoTrackSelection trackSelection;
private DashManifest manifest;
private int periodIndex;
@Nullable private IOException fatalError;
@@ -152,7 +152,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
DashManifest manifest,
int periodIndex,
int[] adaptationSetIndices,
- TrackSelection trackSelection,
+ ExoTrackSelection trackSelection,
int trackType,
DataSource dataSource,
long elapsedRealtimeOffsetMs,
@@ -228,7 +228,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
@Override
- public void updateTrackSelection(TrackSelection trackSelection) {
+ public void updateTrackSelection(ExoTrackSelection trackSelection) {
this.trackSelection = trackSelection;
}
diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java
index a21e73b0ab..99fd169437 100644
--- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java
+++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java
@@ -201,7 +201,7 @@ public final class DashMediaPeriodTest {
periodIndex,
mock(DashChunkSource.Factory.class),
mock(TransferListener.class),
- DrmSessionManager.getDummyDrmSessionManager(),
+ DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher()
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
mock(LoadErrorHandlingPolicy.class),
diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java
index b2fae93bca..bfc11cb47a 100644
--- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java
+++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java
@@ -42,6 +42,6 @@ public final class DownloadHelperTest {
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT,
(handler, videoListener, audioListener, text, metadata) -> new Renderer[0],
new FakeDataSource.Factory(),
- /* drmSessionManager= */ DrmSessionManager.getDummyDrmSessionManager());
+ /* drmSessionManager= */ DrmSessionManager.DRM_UNSUPPORTED);
}
}
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java
index da38bb19ce..3dbbc85d84 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.java
@@ -48,6 +48,7 @@ public final class JpegExtractor implements Extractor {
STATE_READING_SEGMENT_LENGTH,
STATE_READING_SEGMENT,
STATE_SNIFFING_MOTION_PHOTO_VIDEO,
+ STATE_READING_MOTION_PHOTO_VIDEO,
STATE_ENDED,
})
private @interface State {}
@@ -56,7 +57,8 @@ public final class JpegExtractor implements Extractor {
private static final int STATE_READING_SEGMENT_LENGTH = 1;
private static final int STATE_READING_SEGMENT = 2;
private static final int STATE_SNIFFING_MOTION_PHOTO_VIDEO = 4;
- private static final int STATE_ENDED = 5;
+ private static final int STATE_READING_MOTION_PHOTO_VIDEO = 5;
+ private static final int STATE_ENDED = 6;
private static final int JPEG_EXIF_HEADER_LENGTH = 12;
private static final long EXIF_HEADER = 0x45786966; // Exif
@@ -65,6 +67,12 @@ public final class JpegExtractor implements Extractor {
private static final int MARKER_APP1 = 0xFFE1; // Application data 1 marker
private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/";
+ /**
+ * The identifier to use for the image track. Chosen to avoid colliding with track IDs used by
+ * {@link Mp4Extractor} for motion photos.
+ */
+ private static final int IMAGE_TRACK_ID = 1024;
+
private final ParsableByteArray scratch;
private @MonotonicNonNull ExtractorOutput extractorOutput;
@@ -72,11 +80,16 @@ public final class JpegExtractor implements Extractor {
@State private int state;
private int marker;
private int segmentLength;
+ private long mp4StartPosition;
@Nullable private MotionPhotoMetadata motionPhotoMetadata;
+ private @MonotonicNonNull ExtractorInput lastExtractorInput;
+ private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput;
+ private @MonotonicNonNull Mp4Extractor mp4Extractor;
public JpegExtractor() {
scratch = new ParsableByteArray(JPEG_EXIF_HEADER_LENGTH);
+ mp4StartPosition = C.POSITION_UNSET;
}
@Override
@@ -109,12 +122,25 @@ public final class JpegExtractor implements Extractor {
readSegment(input);
return RESULT_CONTINUE;
case STATE_SNIFFING_MOTION_PHOTO_VIDEO:
- if (input.getPosition() != checkNotNull(motionPhotoMetadata).videoStartPosition) {
- seekPosition.position = motionPhotoMetadata.videoStartPosition;
+ if (input.getPosition() != mp4StartPosition) {
+ seekPosition.position = mp4StartPosition;
return RESULT_SEEK;
}
sniffMotionPhotoVideo(input);
return RESULT_CONTINUE;
+ case STATE_READING_MOTION_PHOTO_VIDEO:
+ if (mp4ExtractorStartOffsetExtractorInput == null || input != lastExtractorInput) {
+ lastExtractorInput = input;
+ mp4ExtractorStartOffsetExtractorInput =
+ new StartOffsetExtractorInput(input, mp4StartPosition);
+ }
+ @ReadResult
+ int readResult =
+ checkNotNull(mp4Extractor).read(mp4ExtractorStartOffsetExtractorInput, seekPosition);
+ if (readResult == RESULT_SEEK) {
+ seekPosition.position += mp4StartPosition;
+ }
+ return readResult;
case STATE_ENDED:
return RESULT_END_OF_INPUT;
default:
@@ -124,24 +150,29 @@ public final class JpegExtractor implements Extractor {
@Override
public void seek(long position, long timeUs) {
- state = STATE_READING_MARKER;
+ if (position == 0) {
+ state = STATE_READING_MARKER;
+ } else if (state == STATE_READING_MOTION_PHOTO_VIDEO) {
+ checkNotNull(mp4Extractor).seek(position, timeUs);
+ }
}
@Override
public void release() {
- // Do nothing.
+ if (mp4Extractor != null) {
+ mp4Extractor.release();
+ }
}
private void readMarker(ExtractorInput input) throws IOException {
- scratch.reset(2);
+ scratch.reset(/* limit= */ 2);
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
marker = scratch.readUnsignedShort();
if (marker == MARKER_SOS) { // Start of scan.
- if (motionPhotoMetadata != null) {
+ if (mp4StartPosition != C.POSITION_UNSET) {
state = STATE_SNIFFING_MOTION_PHOTO_VIDEO;
} else {
- outputTracks();
- state = STATE_ENDED;
+ endReadingWithImageTrack();
}
} else if ((marker < 0xFFD0 || marker > 0xFFD9) && marker != 0xFF01) {
state = STATE_READING_SEGMENT_LENGTH;
@@ -164,6 +195,9 @@ public final class JpegExtractor implements Extractor {
@Nullable String xmpString = payload.readNullTerminatedString();
if (xmpString != null) {
motionPhotoMetadata = getMotionPhotoMetadata(xmpString, input.getLength());
+ if (motionPhotoMetadata != null) {
+ mp4StartPosition = motionPhotoMetadata.videoStartPosition;
+ }
}
}
} else {
@@ -178,29 +212,41 @@ public final class JpegExtractor implements Extractor {
input.peekFully(
scratch.getData(), /* offset= */ 0, /* length= */ 1, /* allowEndOfInput= */ true);
if (!peekedData) {
- outputTracks();
+ endReadingWithImageTrack();
} else {
input.resetPeekPosition();
- long mp4StartPosition = input.getPosition();
- StartOffsetExtractorInput mp4ExtractorInput =
+ if (mp4Extractor == null) {
+ mp4Extractor = new Mp4Extractor();
+ }
+ mp4ExtractorStartOffsetExtractorInput =
new StartOffsetExtractorInput(input, mp4StartPosition);
- Mp4Extractor mp4Extractor = new Mp4Extractor();
- if (mp4Extractor.sniff(mp4ExtractorInput)) {
- outputTracks(checkNotNull(motionPhotoMetadata));
+ if (mp4Extractor.sniff(mp4ExtractorStartOffsetExtractorInput)) {
+ mp4Extractor.init(
+ new StartOffsetExtractorOutput(mp4StartPosition, checkNotNull(extractorOutput)));
+ startReadingMotionPhoto();
} else {
- outputTracks();
+ endReadingWithImageTrack();
}
}
+ }
+
+ private void startReadingMotionPhoto() {
+ outputImageTrack(checkNotNull(motionPhotoMetadata));
+ state = STATE_READING_MOTION_PHOTO_VIDEO;
+ }
+
+ private void endReadingWithImageTrack() {
+ outputImageTrack();
+ checkNotNull(extractorOutput).endTracks();
+ extractorOutput.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET));
state = STATE_ENDED;
}
- private void outputTracks(Metadata.Entry... metadataEntries) {
+ private void outputImageTrack(Metadata.Entry... metadataEntries) {
TrackOutput imageTrackOutput =
- checkNotNull(extractorOutput).track(/* id= */ 0, C.TRACK_TYPE_IMAGE);
+ checkNotNull(extractorOutput).track(IMAGE_TRACK_ID, C.TRACK_TYPE_IMAGE);
imageTrackOutput.format(
new Format.Builder().setMetadata(new Metadata(metadataEntries)).build());
- extractorOutput.endTracks();
- extractorOutput.seekMap(new SeekMap.Unseekable(/* durationUs= */ C.TIME_UNSET));
}
/**
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorInput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorInput.java
index 225a408387..132660349b 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorInput.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorInput.java
@@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.jpeg;
-import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ForwardingExtractorInput;
@@ -38,10 +38,12 @@ import com.google.android.exoplayer2.extractor.ForwardingExtractorInput;
* @param input The extractor input to wrap. The reading position must be at or after the start
* offset, otherwise data could be read from before the start offset.
* @param startOffset The offset from which this extractor input provides data, in bytes.
+ * @throws IllegalArgumentException Thrown if the start offset is before the current reading
+ * position.
*/
public StartOffsetExtractorInput(ExtractorInput input, long startOffset) {
super(input);
- checkState(input.getPosition() >= startOffset);
+ checkArgument(input.getPosition() >= startOffset);
this.startOffset = startOffset;
}
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorOutput.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorOutput.java
new file mode 100644
index 0000000000..d0c4730fcb
--- /dev/null
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/jpeg/StartOffsetExtractorOutput.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.jpeg;
+
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+
+/**
+ * An extractor output that wraps another extractor output and applies a give start byte offset to
+ * seek positions.
+ *
+ * This is useful for extracting from a container that's concatenated after some prefix data but
+ * where the container's extractor doesn't handle a non-zero start offset (for example, because it
+ * seeks to absolute positions read from the container data).
+ */
+public final class StartOffsetExtractorOutput implements ExtractorOutput {
+
+ private final long startOffset;
+ private final ExtractorOutput extractorOutput;
+
+ /** Creates a new wrapper reading from the given start byte offset. */
+ public StartOffsetExtractorOutput(long startOffset, ExtractorOutput extractorOutput) {
+ this.startOffset = startOffset;
+ this.extractorOutput = extractorOutput;
+ }
+
+ @Override
+ public TrackOutput track(int id, int type) {
+ return extractorOutput.track(id, type);
+ }
+
+ @Override
+ public void endTracks() {
+ extractorOutput.endTracks();
+ }
+
+ @Override
+ public void seekMap(SeekMap seekMap) {
+ extractorOutput.seekMap(
+ new SeekMap() {
+ @Override
+ public boolean isSeekable() {
+ return seekMap.isSeekable();
+ }
+
+ @Override
+ public long getDurationUs() {
+ return seekMap.getDurationUs();
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ SeekPoints seekPoints = seekMap.getSeekPoints(timeUs);
+ return new SeekPoints(
+ new SeekPoint(seekPoints.first.timeUs, seekPoints.first.position + startOffset),
+ new SeekPoint(seekPoints.second.timeUs, seekPoints.second.position + startOffset));
+ }
+ });
+ }
+}
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 53a6fbabea..c3f3e5e901 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -2445,18 +2445,18 @@ public class MatroskaExtractor implements Extractor {
}
int offset = 1;
int vorbisInfoLength = 0;
- while (codecPrivate[offset] == (byte) 0xFF) {
+ while ((codecPrivate[offset] & 0xFF) == 0xFF) {
vorbisInfoLength += 0xFF;
offset++;
}
- vorbisInfoLength += codecPrivate[offset++];
+ vorbisInfoLength += codecPrivate[offset++] & 0xFF;
int vorbisSkipLength = 0;
- while (codecPrivate[offset] == (byte) 0xFF) {
+ while ((codecPrivate[offset] & 0xFF) == 0xFF) {
vorbisSkipLength += 0xFF;
offset++;
}
- vorbisSkipLength += codecPrivate[offset++];
+ vorbisSkipLength += codecPrivate[offset++] & 0xFF;
if (codecPrivate[offset] != 0x01) {
throw new ParserException("Error parsing vorbis codec private");
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
index 0ef719c961..7b8cabeb00 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -335,6 +335,26 @@ public final class H265Reader implements ElementaryStreamReader {
Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc);
}
}
+ if (bitArray.readBit()) { // overscan_info_present_flag
+ bitArray.skipBit(); // overscan_appropriate_flag
+ }
+ if (bitArray.readBit()) { // video_signal_type_present_flag
+ bitArray.skipBits(4); // video_format, video_full_range_flag
+ if (bitArray.readBit()) { // colour_description_present_flag
+ // colour_primaries, transfer_characteristics, matrix_coeffs
+ bitArray.skipBits(24);
+ }
+ }
+ if (bitArray.readBit()) { // chroma_loc_info_present_flag
+ bitArray.readUnsignedExpGolombCodedInt(); // chroma_sample_loc_type_top_field
+ bitArray.readUnsignedExpGolombCodedInt(); // chroma_sample_loc_type_bottom_field
+ }
+ bitArray.skipBit(); // neutral_chroma_indication_flag
+ if (bitArray.readBit()) { // field_seq_flag
+ // field_seq_flag equal to 1 indicates that the coded video sequence conveys pictures that
+ // represent fields, which means that frame height is double the picture height.
+ picHeightInLumaSamples *= 2;
+ }
}
// Parse the SPS to derive an RFC 6381 codecs string.
diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
index 4387993f50..af8ede69aa 100644
--- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
+++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
@@ -115,7 +115,7 @@ import java.io.IOException;
input.resetPeekPosition();
ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
- // Skip all chunks until we hit the data header.
+ // Skip all chunks until we find the data header.
ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
while (chunkHeader.id != WavUtil.DATA_FOURCC) {
if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) {
diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
index 16bcfc2f76..64faff9a0e 100644
--- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
+++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
@@ -67,6 +67,12 @@ public final class MatroskaExtractorTest {
simulationConfig);
}
+ @Test
+ public void mkvSample_withVorbisAudio() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ MatroskaExtractor::new, "media/mkv/sample_with_vorbis_audio.mkv", simulationConfig);
+ }
+
@Test
public void mkvSample_withHtcRotationInfoInTrackName() throws Exception {
ExtractorAsserts.assertBehavior(
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
index f506403713..66cd100a63 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -37,7 +37,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
@@ -137,8 +137,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Note: The track group in the selection is typically *not* equal to trackGroup. This is due to
// the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods
- // in TrackSelection to avoid unexpected behavior.
- private TrackSelection trackSelection;
+ // in ExoTrackSelection to avoid unexpected behavior.
+ private ExoTrackSelection trackSelection;
private long liveEdgeInPeriodTimeUs;
private boolean seenExpectedPlaylistError;
@@ -219,14 +219,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Sets the current track selection.
*
- * @param trackSelection The {@link TrackSelection}.
+ * @param trackSelection The {@link ExoTrackSelection}.
*/
- public void setTrackSelection(TrackSelection trackSelection) {
+ public void setTrackSelection(ExoTrackSelection trackSelection) {
this.trackSelection = trackSelection;
}
- /** Returns the current {@link TrackSelection}. */
- public TrackSelection getTrackSelection() {
+ /** Returns the current {@link ExoTrackSelection}. */
+ public ExoTrackSelection getTrackSelection() {
return trackSelection;
}
@@ -810,9 +810,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Private classes.
- /**
- * A {@link TrackSelection} to use for initialization.
- */
+ /** A {@link ExoTrackSelection} to use for initialization. */
private static final class InitializationTrackSelection extends BaseTrackSelection {
private int selectedIndex;
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 1ef6ab3afa..9d643ea926 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
@@ -39,7 +39,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Rendition;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
@@ -177,7 +177,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
// null URLs, this method must be updated to calculate stream keys that are compatible with those
// that may already be persisted for offline.
@Override
- public List getStreamKeys(List trackSelections) {
+ public List getStreamKeys(List trackSelections) {
// See HlsMasterPlaylist.copy for interpretation of StreamKeys.
HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
boolean hasVariants = !masterPlaylist.variants.isEmpty();
@@ -202,7 +202,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
List streamKeys = new ArrayList<>();
boolean needsPrimaryTrackGroupSelection = false;
boolean hasPrimaryTrackGroupSelection = false;
- for (TrackSelection trackSelection : trackSelections) {
+ for (ExoTrackSelection trackSelection : trackSelections) {
TrackGroup trackSelectionGroup = trackSelection.getTrackGroup();
int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup);
if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) {
@@ -258,7 +258,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -286,7 +286,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
// Select tracks for each child, copying the resulting streams back into a new streams array.
SampleStream[] newStreams = new SampleStream[selections.length];
@NullableType SampleStream[] childStreams = new SampleStream[selections.length];
- @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length];
+ @NullableType ExoTrackSelection[] childSelections = new ExoTrackSelection[selections.length];
int newEnabledSampleStreamWrapperCount = 0;
HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers =
new HlsSampleStreamWrapper[sampleStreamWrappers.length];
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
index 331e8232cb..e5c233ef43 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
@@ -26,8 +26,10 @@ import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource;
@@ -35,7 +37,6 @@ import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.SequenceableLoader;
@@ -94,13 +95,13 @@ public final class HlsMediaSource extends BaseMediaSource
public static final class Factory implements MediaSourceFactory {
private final HlsDataSourceFactory hlsDataSourceFactory;
- private final MediaSourceDrmHelper mediaSourceDrmHelper;
private HlsExtractorFactory extractorFactory;
private HlsPlaylistParserFactory playlistParserFactory;
private HlsPlaylistTracker.Factory playlistTrackerFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
- @Nullable private DrmSessionManager drmSessionManager;
+ private boolean usingCustomDrmSessionManagerProvider;
+ private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private boolean allowChunklessPreparation;
@MetadataType private int metadataType;
@@ -128,7 +129,7 @@ public final class HlsMediaSource extends BaseMediaSource
*/
public Factory(HlsDataSourceFactory hlsDataSourceFactory) {
this.hlsDataSourceFactory = checkNotNull(hlsDataSourceFactory);
- mediaSourceDrmHelper = new MediaSourceDrmHelper();
+ drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
playlistParserFactory = new DefaultHlsPlaylistParserFactory();
playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY;
extractorFactory = HlsExtractorFactory.DEFAULT;
@@ -280,22 +281,44 @@ public final class HlsMediaSource extends BaseMediaSource
return this;
}
+ @Override
+ public Factory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ if (drmSessionManagerProvider != null) {
+ this.drmSessionManagerProvider = drmSessionManagerProvider;
+ this.usingCustomDrmSessionManagerProvider = true;
+ } else {
+ this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
+ this.usingCustomDrmSessionManagerProvider = false;
+ }
+ return this;
+ }
+
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
- this.drmSessionManager = drmSessionManager;
+ if (drmSessionManager == null) {
+ setDrmSessionManagerProvider(null);
+ } else {
+ setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
+ }
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
- mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider)
+ .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ }
return this;
}
@Override
- public MediaSourceFactory setDrmUserAgent(@Nullable String userAgent) {
- mediaSourceDrmHelper.setDrmUserAgent(userAgent);
+ public Factory setDrmUserAgent(@Nullable String userAgent) {
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent);
+ }
return this;
}
@@ -369,7 +392,7 @@ public final class HlsMediaSource extends BaseMediaSource
hlsDataSourceFactory,
extractorFactory,
compositeSequenceableLoaderFactory,
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
+ drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
playlistTrackerFactory.createTracker(
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
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 7d553fd57f..df1c598be5 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
@@ -53,7 +53,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataReader;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@@ -331,7 +331,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* part of the track selection.
*/
public boolean selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -358,11 +358,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
: positionUs != lastSeekPositionUs);
// Get the old (i.e. current before the loop below executes) primary track selection. The new
// primary selection will equal the old one unless it's changed in the loop.
- TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection();
- TrackSelection primaryTrackSelection = oldPrimaryTrackSelection;
+ ExoTrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection();
+ ExoTrackSelection primaryTrackSelection = oldPrimaryTrackSelection;
// Select new tracks.
for (int i = 0; i < selections.length; i++) {
- TrackSelection selection = selections[i];
+ ExoTrackSelection selection = selections[i];
if (selection == null) {
continue;
}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
index 868cea7fd0..be9aed4393 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
@@ -34,7 +34,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
@@ -61,7 +61,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int elementIndex,
- TrackSelection trackSelection,
+ ExoTrackSelection trackSelection,
@Nullable TransferListener transferListener) {
DataSource dataSource = dataSourceFactory.createDataSource();
if (transferListener != null) {
@@ -78,7 +78,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
private final ChunkExtractor[] chunkExtractors;
private final DataSource dataSource;
- private TrackSelection trackSelection;
+ private ExoTrackSelection trackSelection;
private SsManifest manifest;
private int currentManifestChunkOffset;
@@ -95,7 +95,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int streamElementIndex,
- TrackSelection trackSelection,
+ ExoTrackSelection trackSelection,
DataSource dataSource) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
@@ -163,7 +163,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
}
@Override
- public void updateTrackSelection(TrackSelection trackSelection) {
+ public void updateTrackSelection(ExoTrackSelection trackSelection) {
this.trackSelection = trackSelection;
}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java
index 111393140e..875b1379c9 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java
@@ -18,13 +18,11 @@ package com.google.android.exoplayer2.source.smoothstreaming;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.chunk.ChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener;
-/**
- * A {@link ChunkSource} for SmoothStreaming.
- */
+/** A {@link ChunkSource} for SmoothStreaming. */
public interface SsChunkSource extends ChunkSource {
/** Factory for {@link SsChunkSource}s. */
@@ -45,7 +43,7 @@ public interface SsChunkSource extends ChunkSource {
LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int streamElementIndex,
- TrackSelection trackSelection,
+ ExoTrackSelection trackSelection,
@Nullable TransferListener transferListener);
}
@@ -61,5 +59,5 @@ public interface SsChunkSource extends ChunkSource {
*
* @param trackSelection The new track selection instance. Must be equivalent to the previous one.
*/
- void updateTrackSelection(TrackSelection trackSelection);
+ void updateTrackSelection(ExoTrackSelection trackSelection);
}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
index b6e21cd870..ae96b941d2 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
@@ -31,7 +31,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
@@ -123,7 +123,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public long selectTracks(
- @NullableType TrackSelection[] selections,
+ @NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
@@ -156,10 +156,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
- public List getStreamKeys(List trackSelections) {
+ public List getStreamKeys(List trackSelections) {
List streamKeys = new ArrayList<>();
for (int selectionIndex = 0; selectionIndex < trackSelections.size(); selectionIndex++) {
- TrackSelection trackSelection = trackSelections.get(selectionIndex);
+ ExoTrackSelection trackSelection = trackSelections.get(selectionIndex);
int streamElementIndex = trackGroups.indexOf(trackSelection.getTrackGroup());
for (int i = 0; i < trackSelection.length(); i++) {
streamKeys.add(new StreamKey(streamElementIndex, trackSelection.getIndexInTrackGroup(i)));
@@ -232,16 +232,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// Private methods.
- private ChunkSampleStream buildSampleStream(TrackSelection selection,
- long positionUs) {
+ private ChunkSampleStream buildSampleStream(
+ ExoTrackSelection selection, long positionUs) {
int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup());
SsChunkSource chunkSource =
chunkSourceFactory.createChunkSource(
- manifestLoaderErrorThrower,
- manifest,
- streamElementIndex,
- selection,
- transferListener);
+ manifestLoaderErrorThrower, manifest, streamElementIndex, selection, transferListener);
return new ChunkSampleStream<>(
manifest.streamElements[streamElementIndex].type,
null,
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
index af128d848a..bd6f5df197 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
@@ -27,8 +27,10 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.BaseMediaSource;
@@ -38,7 +40,6 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.source.MediaSourceDrmHelper;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaSourceFactory;
@@ -78,11 +79,11 @@ public final class SsMediaSource extends BaseMediaSource
public static final class Factory implements MediaSourceFactory {
private final SsChunkSource.Factory chunkSourceFactory;
- private final MediaSourceDrmHelper mediaSourceDrmHelper;
@Nullable private final DataSource.Factory manifestDataSourceFactory;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
- @Nullable private DrmSessionManager drmSessionManager;
+ private boolean usingCustomDrmSessionManagerProvider;
+ private DrmSessionManagerProvider drmSessionManagerProvider;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
@Nullable private ParsingLoadable.Parser extends SsManifest> manifestParser;
@@ -113,7 +114,7 @@ public final class SsMediaSource extends BaseMediaSource
@Nullable DataSource.Factory manifestDataSourceFactory) {
this.chunkSourceFactory = checkNotNull(chunkSourceFactory);
this.manifestDataSourceFactory = manifestDataSourceFactory;
- mediaSourceDrmHelper = new MediaSourceDrmHelper();
+ drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
@@ -191,22 +192,44 @@ public final class SsMediaSource extends BaseMediaSource
return this;
}
+ @Override
+ public Factory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ if (drmSessionManagerProvider != null) {
+ this.drmSessionManagerProvider = drmSessionManagerProvider;
+ this.usingCustomDrmSessionManagerProvider = true;
+ } else {
+ this.drmSessionManagerProvider = new DefaultDrmSessionManagerProvider();
+ this.usingCustomDrmSessionManagerProvider = false;
+ }
+ return this;
+ }
+
@Override
public Factory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) {
- this.drmSessionManager = drmSessionManager;
+ if (drmSessionManager == null) {
+ setDrmSessionManagerProvider(null);
+ } else {
+ setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager);
+ }
return this;
}
@Override
public Factory setDrmHttpDataSourceFactory(
@Nullable HttpDataSource.Factory drmHttpDataSourceFactory) {
- mediaSourceDrmHelper.setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider)
+ .setDrmHttpDataSourceFactory(drmHttpDataSourceFactory);
+ }
return this;
}
@Override
public Factory setDrmUserAgent(@Nullable String userAgent) {
- mediaSourceDrmHelper.setDrmUserAgent(userAgent);
+ if (!usingCustomDrmSessionManagerProvider) {
+ ((DefaultDrmSessionManagerProvider) drmSessionManagerProvider).setDrmUserAgent(userAgent);
+ }
return this;
}
@@ -277,7 +300,7 @@ public final class SsMediaSource extends BaseMediaSource
/* manifestParser= */ null,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
+ drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs);
}
@@ -321,7 +344,7 @@ public final class SsMediaSource extends BaseMediaSource
manifestParser,
chunkSourceFactory,
compositeSequenceableLoaderFactory,
- drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
+ drmSessionManagerProvider.get(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs);
}
diff --git a/library/transformer/README.md b/library/transformer/README.md
new file mode 100644
index 0000000000..5de22fa583
--- /dev/null
+++ b/library/transformer/README.md
@@ -0,0 +1,10 @@
+# ExoPlayer transformer library module #
+
+Provides support for transforming media files.
+
+## Links ##
+
+* [Javadoc][]: Classes matching `com.google.android.exoplayer2.transformer.*`
+ belong to this module.
+
+[Javadoc]: https://exoplayer.dev/doc/reference/index.html
diff --git a/library/transformer/build.gradle b/library/transformer/build.gradle
new file mode 100644
index 0000000000..6870c9f577
--- /dev/null
+++ b/library/transformer/build.gradle
@@ -0,0 +1,47 @@
+// Copyright 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = true
+ }
+ }
+
+ sourceSets.test.assets.srcDir '../../testdata/src/test/assets/'
+}
+
+dependencies {
+ implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ implementation project(modulePrefix + 'library-core')
+ compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
+ compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
+ compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
+ testImplementation project(modulePrefix + 'robolectricutils')
+ testImplementation project(modulePrefix + 'testutils')
+ testImplementation project(modulePrefix + 'testdata')
+ testImplementation 'org.robolectric:robolectric:' + robolectricVersion
+}
+
+ext {
+ javadocTitle = 'Transformer module'
+}
+apply from: '../../javadoc_library.gradle'
+
+ext {
+ releaseArtifact = 'exoplayer-transformer'
+ releaseDescription = 'The ExoPlayer library transformer module.'
+}
+apply from: '../../publish.gradle'
diff --git a/library/transformer/src/main/AndroidManifest.xml b/library/transformer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..3c3792d7a2
--- /dev/null
+++ b/library/transformer/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java
new file mode 100644
index 0000000000..5452af4296
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/FrameworkMuxer.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Util.SDK_INT;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.ParcelFileDescriptor;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.mediacodec.MediaFormatUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+
+/** Muxer implementation that uses a {@link MediaMuxer}. */
+@RequiresApi(18)
+/* package */ final class FrameworkMuxer implements Muxer {
+
+ public static final class Factory implements Muxer.Factory {
+ @Override
+ public FrameworkMuxer create(String path, String outputMimeType) throws IOException {
+ MediaMuxer mediaMuxer = new MediaMuxer(path, mimeTypeToMuxerOutputFormat(outputMimeType));
+ return new FrameworkMuxer(mediaMuxer, outputMimeType);
+ }
+
+ @RequiresApi(26)
+ @Override
+ public FrameworkMuxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType)
+ throws IOException {
+ MediaMuxer mediaMuxer =
+ new MediaMuxer(
+ parcelFileDescriptor.getFileDescriptor(),
+ mimeTypeToMuxerOutputFormat(outputMimeType));
+ return new FrameworkMuxer(mediaMuxer, outputMimeType);
+ }
+
+ @Override
+ public boolean supportsOutputMimeType(String mimeType) {
+ try {
+ mimeTypeToMuxerOutputFormat(mimeType);
+ } catch (IllegalStateException e) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private final MediaMuxer mediaMuxer;
+ private final String outputMimeType;
+ private final MediaCodec.BufferInfo bufferInfo;
+
+ private boolean isStarted;
+
+ private FrameworkMuxer(MediaMuxer mediaMuxer, String outputMimeType) {
+ this.mediaMuxer = mediaMuxer;
+ this.outputMimeType = outputMimeType;
+ bufferInfo = new MediaCodec.BufferInfo();
+ }
+
+ @Override
+ public boolean supportsSampleMimeType(@Nullable String mimeType) {
+ // MediaMuxer supported sample formats are documented in MediaMuxer.addTrack(MediaFormat).
+ boolean isAudio = MimeTypes.isAudio(mimeType);
+ boolean isVideo = MimeTypes.isVideo(mimeType);
+ if (outputMimeType.equals(MimeTypes.VIDEO_MP4)) {
+ if (isVideo) {
+ return MimeTypes.VIDEO_H263.equals(mimeType)
+ || MimeTypes.VIDEO_H264.equals(mimeType)
+ || MimeTypes.VIDEO_MP4V.equals(mimeType)
+ || (Util.SDK_INT >= 24 && MimeTypes.VIDEO_H265.equals(mimeType));
+ } else if (isAudio) {
+ return MimeTypes.AUDIO_AAC.equals(mimeType)
+ || MimeTypes.AUDIO_AMR_NB.equals(mimeType)
+ || MimeTypes.AUDIO_AMR_WB.equals(mimeType);
+ }
+ } else if (outputMimeType.equals(MimeTypes.VIDEO_WEBM) && SDK_INT >= 21) {
+ if (isVideo) {
+ return MimeTypes.VIDEO_VP8.equals(mimeType)
+ || (Util.SDK_INT >= 24 && MimeTypes.VIDEO_VP9.equals(mimeType));
+ } else if (isAudio) {
+ return MimeTypes.AUDIO_VORBIS.equals(mimeType);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int addTrack(Format format) {
+ String sampleMimeType = checkNotNull(format.sampleMimeType);
+ MediaFormat mediaFormat;
+ if (MimeTypes.isAudio(sampleMimeType)) {
+ mediaFormat =
+ MediaFormat.createAudioFormat(
+ castNonNull(sampleMimeType), format.sampleRate, format.channelCount);
+ } else {
+ mediaFormat =
+ MediaFormat.createVideoFormat(castNonNull(sampleMimeType), format.width, format.height);
+ mediaMuxer.setOrientationHint(format.rotationDegrees);
+ }
+ MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
+ return mediaMuxer.addTrack(mediaFormat);
+ }
+
+ @Override
+ public void writeSampleData(
+ int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) {
+ if (!isStarted) {
+ isStarted = true;
+ mediaMuxer.start();
+ }
+ int offset = data.position();
+ int size = data.limit() - offset;
+ int flags = isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0;
+ bufferInfo.set(offset, size, presentationTimeUs, flags);
+ mediaMuxer.writeSampleData(trackIndex, data, bufferInfo);
+ }
+
+ @Override
+ public void release(boolean forCancellation) {
+ if (!isStarted) {
+ mediaMuxer.release();
+ return;
+ }
+
+ isStarted = false;
+ try {
+ mediaMuxer.stop();
+ } catch (IllegalStateException e) {
+ if (SDK_INT < 30) {
+ // Set the muxer state to stopped even if mediaMuxer.stop() failed so that
+ // mediaMuxer.release() doesn't attempt to stop the muxer and therefore doesn't throw the
+ // same exception without releasing its resources. This is already implemented in MediaMuxer
+ // from API level 30.
+ try {
+ Field muxerStoppedStateField = MediaMuxer.class.getDeclaredField("MUXER_STATE_STOPPED");
+ muxerStoppedStateField.setAccessible(true);
+ int muxerStoppedState = castNonNull((Integer) muxerStoppedStateField.get(mediaMuxer));
+ Field muxerStateField = MediaMuxer.class.getDeclaredField("mState");
+ muxerStateField.setAccessible(true);
+ muxerStateField.set(mediaMuxer, muxerStoppedState);
+ } catch (Exception reflectionException) {
+ // Do nothing.
+ }
+ }
+ // It doesn't matter that stopping the muxer throws if the transformation is being cancelled.
+ if (!forCancellation) {
+ throw e;
+ }
+ } finally {
+ mediaMuxer.release();
+ }
+ }
+
+ /**
+ * Converts a {@link MimeTypes MIME type} into a {@link MediaMuxer.OutputFormat MediaMuxer output
+ * format}.
+ *
+ * @param mimeType The {@link MimeTypes MIME type} to convert.
+ * @return The corresponding {@link MediaMuxer.OutputFormat MediaMuxer output format}.
+ * @throws IllegalArgumentException If the {@link MimeTypes MIME type} is not supported as output
+ * format.
+ */
+ private static int mimeTypeToMuxerOutputFormat(String mimeType) {
+ if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
+ return MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
+ } else if (SDK_INT >= 21 && mimeType.equals(MimeTypes.VIDEO_WEBM)) {
+ return MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
+ } else {
+ throw new IllegalArgumentException("Unsupported output MIME type: " + mimeType);
+ }
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java
new file mode 100644
index 0000000000..bf8f7f3aae
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MediaCodecAdapterWrapper.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaFormat;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
+import com.google.android.exoplayer2.mediacodec.MediaFormatUtil;
+import com.google.android.exoplayer2.mediacodec.SynchronousMediaCodecAdapter;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A wrapper around {@link MediaCodecAdapter}.
+ *
+ * Provides a layer of abstraction for callers that need to interact with {@link MediaCodec}
+ * through {@link MediaCodecAdapter}. This is done by simplifying the calls needed to queue and
+ * dequeue buffers, removing the need to track buffer indices and codec events.
+ */
+/* package */ final class MediaCodecAdapterWrapper {
+
+ // MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float.
+ // https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers.
+ private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT;
+
+ private final BufferInfo outputBufferInfo;
+ private final MediaCodecAdapter codec;
+
+ private @MonotonicNonNull Format outputFormat;
+ @Nullable private ByteBuffer outputBuffer;
+
+ private int inputBufferIndex;
+ private int outputBufferIndex;
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+
+ /**
+ * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link
+ * MediaCodecAdapter} audio decoder.
+ *
+ * @param format The {@link Format} (of the input data) used to determine the underlying {@link
+ * MediaCodec} and its configuration values.
+ * @return A configured and started decoder wrapper.
+ * @throws IOException If the underlying codec cannot be created.
+ */
+ public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) throws IOException {
+ @Nullable MediaCodec decoder = null;
+ @Nullable MediaCodecAdapter adapter = null;
+ try {
+ decoder = MediaCodec.createDecoderByType(checkNotNull(format.sampleMimeType));
+ MediaFormat mediaFormat =
+ MediaFormat.createAudioFormat(
+ format.sampleMimeType, format.sampleRate, format.channelCount);
+ MediaFormatUtil.maybeSetInteger(
+ mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
+ MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
+ adapter = new SynchronousMediaCodecAdapter.Factory().createAdapter(decoder);
+ adapter.configure(mediaFormat, /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
+ adapter.start();
+ return new MediaCodecAdapterWrapper(adapter);
+ } catch (Exception e) {
+ if (adapter != null) {
+ adapter.release();
+ } else if (decoder != null) {
+ decoder.release();
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link
+ * MediaCodecAdapter} audio encoder.
+ *
+ * @param format The {@link Format} (of the output data) used to determine the underlying {@link
+ * MediaCodec} and its configuration values.
+ * @return A configured and started encoder wrapper.
+ * @throws IOException If the underlying codec cannot be created.
+ */
+ public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) throws IOException {
+ @Nullable MediaCodec encoder = null;
+ @Nullable MediaCodecAdapter adapter = null;
+ try {
+ encoder = MediaCodec.createEncoderByType(checkNotNull(format.sampleMimeType));
+ MediaFormat mediaFormat =
+ MediaFormat.createAudioFormat(
+ format.sampleMimeType, format.sampleRate, format.channelCount);
+ mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
+ adapter = new SynchronousMediaCodecAdapter.Factory().createAdapter(encoder);
+ adapter.configure(
+ mediaFormat,
+ /* surface= */ null,
+ /* crypto= */ null,
+ /* flags= */ MediaCodec.CONFIGURE_FLAG_ENCODE);
+ adapter.start();
+ return new MediaCodecAdapterWrapper(adapter);
+ } catch (Exception e) {
+ if (adapter != null) {
+ adapter.release();
+ } else if (encoder != null) {
+ encoder.release();
+ }
+ throw e;
+ }
+ }
+
+ private MediaCodecAdapterWrapper(MediaCodecAdapter codec) {
+ this.codec = codec;
+ outputBufferInfo = new BufferInfo();
+ inputBufferIndex = C.INDEX_UNSET;
+ outputBufferIndex = C.INDEX_UNSET;
+ }
+
+ /**
+ * Dequeues a writable input buffer, if available.
+ *
+ * @param inputBuffer The buffer where the dequeued buffer data is stored.
+ * @return Whether an input buffer is ready to be used.
+ */
+ @EnsuresNonNullIf(expression = "#1.data", result = true)
+ public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) {
+ if (inputStreamEnded) {
+ return false;
+ }
+ if (inputBufferIndex < 0) {
+ inputBufferIndex = codec.dequeueInputBufferIndex();
+ if (inputBufferIndex < 0) {
+ return false;
+ }
+ inputBuffer.data = codec.getInputBuffer(inputBufferIndex);
+ inputBuffer.clear();
+ }
+ checkNotNull(inputBuffer.data);
+ return true;
+ }
+
+ /**
+ * Queues an input buffer to the decoder. No buffers may be queued after an {@link
+ * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued.
+ */
+ public void queueInputBuffer(DecoderInputBuffer inputBuffer) {
+ checkState(
+ !inputStreamEnded, "Input buffer can not be queued after the input stream has ended.");
+
+ int offset = 0;
+ int size = 0;
+ if (inputBuffer.data != null && inputBuffer.data.hasRemaining()) {
+ offset = inputBuffer.data.position();
+ size = inputBuffer.data.remaining();
+ }
+ int flags = 0;
+ if (inputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ }
+ codec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags);
+ inputBufferIndex = C.INDEX_UNSET;
+ inputBuffer.data = null;
+ }
+
+ /** Returns the current output format, if available. */
+ @Nullable
+ public Format getOutputFormat() {
+ // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now.
+ maybeDequeueOutputBuffer();
+ return outputFormat;
+ }
+
+ /** Returns the current output {@link ByteBuffer}, if available. */
+ @Nullable
+ public ByteBuffer getOutputBuffer() {
+ return maybeDequeueOutputBuffer() ? outputBuffer : null;
+ }
+
+ /** Returns the {@link BufferInfo} associated with the current output buffer, if available. */
+ @Nullable
+ public BufferInfo getOutputBufferInfo() {
+ return maybeDequeueOutputBuffer() ? outputBufferInfo : null;
+ }
+
+ /**
+ * Releases the current output buffer.
+ *
+ *
This should be called after the buffer has been processed. The next output buffer will not
+ * be available until the previous has been released.
+ */
+ public void releaseOutputBuffer() {
+ outputBuffer = null;
+ codec.releaseOutputBuffer(outputBufferIndex, /* render= */ false);
+ outputBufferIndex = C.INDEX_UNSET;
+ }
+
+ /** Returns whether the codec output stream has ended, and no more data can be dequeued. */
+ public boolean isEnded() {
+ return outputStreamEnded && outputBufferIndex == C.INDEX_UNSET;
+ }
+
+ /** Releases the underlying codec. */
+ public void release() {
+ outputBuffer = null;
+ codec.release();
+ }
+
+ /**
+ * Returns true if there is already an output buffer pending. Otherwise attempts to dequeue an
+ * output buffer and returns whether there is a new output buffer.
+ */
+ private boolean maybeDequeueOutputBuffer() {
+ if (outputBufferIndex >= 0) {
+ return true;
+ }
+ if (outputStreamEnded) {
+ return false;
+ }
+
+ outputBufferIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
+ if (outputBufferIndex < 0) {
+ if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ outputFormat = getFormat(codec.getOutputFormat());
+ }
+ return false;
+ }
+ if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ outputStreamEnded = true;
+ if (outputBufferInfo.size == 0) {
+ releaseOutputBuffer();
+ return false;
+ }
+ }
+ if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ // Encountered a CSD buffer, skip it.
+ releaseOutputBuffer();
+ return false;
+ }
+
+ outputBuffer = checkNotNull(codec.getOutputBuffer(outputBufferIndex));
+ outputBuffer.position(outputBufferInfo.offset);
+ outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
+
+ return true;
+ }
+
+ private static Format getFormat(MediaFormat mediaFormat) {
+ ImmutableList.Builder csdBuffers = new ImmutableList.Builder<>();
+ int csdIndex = 0;
+ while (true) {
+ @Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex);
+ if (csdByteBuffer == null) {
+ break;
+ }
+ byte[] csdBufferData = new byte[csdByteBuffer.remaining()];
+ csdByteBuffer.get(csdBufferData);
+ csdBuffers.add(csdBufferData);
+ csdIndex++;
+ }
+ String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
+ Format.Builder formatBuilder =
+ new Format.Builder()
+ .setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME))
+ .setInitializationData(csdBuffers.build());
+ if (MimeTypes.isVideo(mimeType)) {
+ formatBuilder
+ .setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH))
+ .setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
+ } else if (MimeTypes.isAudio(mimeType)) {
+ // TODO(internal b/178685617): Only set the PCM encoding for audio/raw, once we have a way to
+ // simulate more realistic codec input/output formats in tests.
+ formatBuilder
+ .setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT))
+ .setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE))
+ .setPcmEncoding(MEDIA_CODEC_PCM_ENCODING);
+ }
+ return formatBuilder.build();
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java
new file mode 100644
index 0000000000..72e5f0f6b8
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Muxer.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import android.os.ParcelFileDescriptor;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Abstracts media muxing operations.
+ *
+ * Query whether {@link #supportsSampleMimeType(String) sample MIME types are supported} and
+ * {@link #addTrack(Format) add all tracks}, then {@link #writeSampleData(int, ByteBuffer, boolean,
+ * long) write sample data} to mux samples. Once any sample data has been written, it is not
+ * possible to add tracks. After writing all sample data, {@link #release() release} the instance to
+ * finish writing to the output and return any resources to the system.
+ */
+/* package */ interface Muxer {
+
+ /** Factory for muxers. */
+ interface Factory {
+ /**
+ * Returns a new muxer writing to a file.
+ *
+ * @param path The path to the output file.
+ * @param outputMimeType The container {@link MimeTypes MIME type} of the output file.
+ * @throws IllegalArgumentException If the path is invalid or the MIME type is not supported.
+ * @throws IOException If an error occurs opening the output file for writing.
+ */
+ Muxer create(String path, String outputMimeType) throws IOException;
+
+ /**
+ * Returns a new muxer writing to a file descriptor.
+ *
+ * @param parcelFileDescriptor A readable and writable {@link ParcelFileDescriptor} of the
+ * output. The file referenced by this ParcelFileDescriptor should not be used before the
+ * muxer is released. It is the responsibility of the caller to close the
+ * ParcelFileDescriptor. This can be done after this method returns.
+ * @param outputMimeType The {@link MimeTypes MIME type} of the output.
+ * @throws IllegalArgumentException If the file descriptor is invalid or the MIME type is not
+ * supported.
+ * @throws IOException If an error occurs opening the output file descriptor for writing.
+ */
+ Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType)
+ throws IOException;
+
+ /** Returns whether the {@link MimeTypes MIME type} provided is a supported output format. */
+ boolean supportsOutputMimeType(String mimeType);
+ }
+
+ /** Returns whether the sample {@link MimeTypes MIME type} is supported. */
+ boolean supportsSampleMimeType(@Nullable String mimeType);
+
+ /**
+ * Adds a track with the specified format, and returns its index (to be passed in subsequent calls
+ * to {@link #writeSampleData(int, ByteBuffer, boolean, long)}).
+ */
+ int addTrack(Format format);
+
+ /**
+ * Writes the specified sample.
+ *
+ * @param trackIndex The index of the track, previously returned by {@link #addTrack(Format)}.
+ * @param data Buffer containing the sample data to write to the container.
+ * @param isKeyFrame Whether the sample is a key frame.
+ * @param presentationTimeUs The presentation time of the sample in microseconds.
+ */
+ void writeSampleData(
+ int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs);
+
+ /**
+ * Releases any resources associated with muxing.
+ *
+ * @param forCancellation Whether the reason for releasing the resources is the transformation
+ * cancellation.
+ */
+ void release(boolean forCancellation);
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java
new file mode 100644
index 0000000000..2e9710dc15
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/MuxerWrapper.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static com.google.android.exoplayer2.util.Util.minValue;
+
+import android.util.SparseIntArray;
+import android.util.SparseLongArray;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.nio.ByteBuffer;
+
+/**
+ * A wrapper around a media muxer.
+ *
+ *
This wrapper can contain at most one video track and one audio track.
+ */
+@RequiresApi(18)
+/* package */ final class MuxerWrapper {
+
+ /**
+ * The maximum difference between the track positions, in microseconds.
+ *
+ *
The value of this constant has been chosen based on the interleaving observed in a few media
+ * files, where continuous chunks of the same track were about 0.5 seconds long.
+ */
+ private static final long MAX_TRACK_WRITE_AHEAD_US = C.msToUs(500);
+
+ private final Muxer muxer;
+ private final SparseIntArray trackTypeToIndex;
+ private final SparseLongArray trackTypeToTimeUs;
+
+ private int trackCount;
+ private int trackFormatCount;
+ private boolean isReady;
+ private int previousTrackType;
+ private long minTrackTimeUs;
+
+ public MuxerWrapper(Muxer muxer) {
+ this.muxer = muxer;
+ trackTypeToIndex = new SparseIntArray();
+ trackTypeToTimeUs = new SparseLongArray();
+ previousTrackType = C.TRACK_TYPE_NONE;
+ }
+
+ /**
+ * Registers an output track.
+ *
+ *
All tracks must be registered before any track format is {@link #addTrackFormat(Format)
+ * added}.
+ *
+ * @throws IllegalStateException If a track format was {@link #addTrackFormat(Format) added}
+ * before calling this method.
+ */
+ public void registerTrack() {
+ checkState(
+ trackFormatCount == 0, "Tracks cannot be registered after track formats have been added.");
+ trackCount++;
+ }
+
+ /** Returns whether the sample {@link MimeTypes MIME type} is supported. */
+ public boolean supportsSampleMimeType(@Nullable String mimeType) {
+ return muxer.supportsSampleMimeType(mimeType);
+ }
+
+ /**
+ * Adds a track format to the muxer.
+ *
+ *
The tracks must all be {@link #registerTrack() registered} before any format is added and
+ * all the formats must be added before samples are {@link #writeSample(int, ByteBuffer, boolean,
+ * long) written}.
+ *
+ * @param format The {@link Format} to be added.
+ * @throws IllegalStateException If the format is unsupported or if there is already a track
+ * format of the same type (audio or video).
+ */
+ public void addTrackFormat(Format format) {
+ checkState(trackCount > 0, "All tracks should be registered before the formats are added.");
+ checkState(trackFormatCount < trackCount, "All track formats have already been added.");
+ @Nullable String sampleMimeType = format.sampleMimeType;
+ boolean isAudio = MimeTypes.isAudio(sampleMimeType);
+ boolean isVideo = MimeTypes.isVideo(sampleMimeType);
+ checkState(isAudio || isVideo, "Unsupported track format: " + sampleMimeType);
+ int trackType = MimeTypes.getTrackType(sampleMimeType);
+ checkState(
+ trackTypeToIndex.get(trackType, /* valueIfKeyNotFound= */ C.INDEX_UNSET) == C.INDEX_UNSET,
+ "There is already a track of type " + trackType);
+
+ int trackIndex = muxer.addTrack(format);
+ trackTypeToIndex.put(trackType, trackIndex);
+ trackTypeToTimeUs.put(trackType, 0L);
+ trackFormatCount++;
+ if (trackFormatCount == trackCount) {
+ isReady = true;
+ }
+ }
+
+ /**
+ * Attempts to write a sample to the muxer.
+ *
+ * @param trackType The track type of the sample, defined by the {@code TRACK_TYPE_*} constants in
+ * {@link C}.
+ * @param data The sample to write, or {@code null} if the sample is empty.
+ * @param isKeyFrame Whether the sample is a key frame.
+ * @param presentationTimeUs The presentation time of the sample in microseconds.
+ * @return Whether the sample was successfully written. This is {@code false} if the muxer hasn't
+ * {@link #addTrackFormat(Format) received a format} for every {@link #registerTrack()
+ * registered track}, or if it should write samples of other track types first to ensure a
+ * good interleaving.
+ * @throws IllegalStateException If the muxer doesn't have any {@link #endTrack(int) non-ended}
+ * track of the given track type.
+ */
+ public boolean writeSample(
+ int trackType, @Nullable ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) {
+ int trackIndex = trackTypeToIndex.get(trackType, /* valueIfKeyNotFound= */ C.INDEX_UNSET);
+ checkState(
+ trackIndex != C.INDEX_UNSET,
+ "Could not write sample because there is no track of type " + trackType);
+
+ if (!canWriteSampleOfType(trackType)) {
+ return false;
+ } else if (data == null) {
+ return true;
+ }
+
+ muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs);
+ trackTypeToTimeUs.put(trackType, presentationTimeUs);
+ previousTrackType = trackType;
+ return true;
+ }
+
+ /**
+ * Notifies the muxer that all the samples have been {@link #writeSample(int, ByteBuffer, boolean,
+ * long) written} for a given track.
+ *
+ * @param trackType The track type, defined by the {@code TRACK_TYPE_*} constants in {@link C}.
+ */
+ public void endTrack(int trackType) {
+ trackTypeToIndex.delete(trackType);
+ trackTypeToTimeUs.delete(trackType);
+ }
+
+ /**
+ * Releases any resources associated with muxing.
+ *
+ *
The muxer cannot be used anymore once this method has been called.
+ *
+ * @param forCancellation Whether the reason for releasing the resources is the transformation
+ * cancellation.
+ */
+ public void release(boolean forCancellation) {
+ isReady = false;
+ muxer.release(forCancellation);
+ }
+
+ /** Returns the number of {@link #registerTrack() registered} tracks. */
+ public int getTrackCount() {
+ return trackCount;
+ }
+
+ /**
+ * Returns whether the muxer can write a sample of the given track type.
+ *
+ * @param trackType The track type, defined by the {@code TRACK_TYPE_*} constants in {@link C}.
+ * @return Whether the muxer can write a sample of the given track type. This is {@code false} if
+ * the muxer hasn't {@link #addTrackFormat(Format) received a format} for every {@link
+ * #registerTrack() registered track}, or if it should write samples of other track types
+ * first to ensure a good interleaving.
+ * @throws IllegalStateException If the muxer doesn't have any {@link #endTrack(int) non-ended}
+ * track of the given track type.
+ */
+ private boolean canWriteSampleOfType(int trackType) {
+ long trackTimeUs = trackTypeToTimeUs.get(trackType, /* valueIfKeyNotFound= */ C.TIME_UNSET);
+ checkState(trackTimeUs != C.TIME_UNSET);
+ if (!isReady) {
+ return false;
+ }
+ if (trackTypeToTimeUs.size() == 1) {
+ return true;
+ }
+ if (trackType != previousTrackType) {
+ minTrackTimeUs = minValue(trackTypeToTimeUs);
+ }
+ return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US;
+ }
+
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ProgressHolder.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ProgressHolder.java
new file mode 100644
index 0000000000..0f34aed821
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ProgressHolder.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import androidx.annotation.IntRange;
+
+/** Holds a progress percentage. */
+public final class ProgressHolder {
+
+ /** The held progress, expressed as an integer percentage. */
+ @IntRange(from = 0, to = 100)
+ public int progress;
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SampleTransformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SampleTransformer.java
new file mode 100644
index 0000000000..266034c905
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SampleTransformer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+
+/** A sample transformer for a given track. */
+/* package */ interface SampleTransformer {
+
+ /**
+ * Transforms the data and metadata of the sample contained in {@code buffer}.
+ *
+ * @param buffer The sample to transform. If the sample {@link DecoderInputBuffer#data data} is
+ * {@code null} after the execution of this method, the sample must be discarded.
+ */
+ void transformSample(DecoderInputBuffer buffer);
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SefSlowMotionVideoSampleTransformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SefSlowMotionVideoSampleTransformer.java
new file mode 100644
index 0000000000..a232d82a52
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SefSlowMotionVideoSampleTransformer.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static com.google.android.exoplayer2.util.NalUnitUtil.NAL_START_CODE;
+import static com.google.android.exoplayer2.util.Util.castNonNull;
+import static java.lang.Math.min;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
+import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.common.collect.ImmutableList;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * {@link SampleTransformer} that flattens SEF slow motion video samples.
+ *
+ *
Such samples follow the ITU-T Recommendation H.264 with temporal SVC.
+ *
+ *
This transformer leaves the samples received unchanged if the input is not an SEF slow motion
+ * video.
+ *
+ *
The mathematical formulas used in this class are explained in [Internal ref:
+ * http://go/exoplayer-sef-slomo-video-flattening].
+ */
+/* package */ final class SefSlowMotionVideoSampleTransformer implements SampleTransformer {
+
+ /**
+ * The frame rate of SEF slow motion videos, in fps.
+ *
+ *
This frame rate is constant and is not equal to the capture frame rate. It is set to a lower
+ * value so that the video is entirely played in slow motion on players that do not support SEF
+ * slow motion.
+ */
+ @VisibleForTesting /* package */ static final int INPUT_FRAME_RATE = 30;
+
+ /**
+ * The target frame rate of the flattened output, in fps.
+ *
+ *
The output frame rate might be slightly different and might not be constant.
+ */
+ private static final int TARGET_OUTPUT_FRAME_RATE = 30;
+
+ private static final int NAL_START_CODE_LENGTH = NAL_START_CODE.length;
+ /**
+ * The nal_unit_type corresponding to a prefix NAL unit (see ITU-T Recommendation H.264 (2016)
+ * table 7-1).
+ */
+ private static final int NAL_UNIT_TYPE_PREFIX = 0x0E;
+
+ private final byte[] scratch;
+ /** The SEF slow motion configuration of the input. */
+ @Nullable private final SlowMotionData slowMotionData;
+ /**
+ * An iterator iterating over the slow motion segments, pointing at the segment following {@code
+ * nextSegmentInfo}, if any.
+ */
+ private final Iterator segmentIterator;
+ /** The frame rate at which the input has been captured, in fps. */
+ private final float captureFrameRate;
+ /** The maximum SVC temporal layer present in the input. */
+ private final int inputMaxLayer;
+ /**
+ * The maximum SVC temporal layer value of the frames that should be kept in the input (or a part
+ * of it) so that it is played at normal speed.
+ */
+ private final int normalSpeedMaxLayer;
+
+ /**
+ * The {@link SegmentInfo} describing the current slow motion segment, or null if the current
+ * frame is not in such a segment.
+ */
+ @Nullable private SegmentInfo currentSegmentInfo;
+ /**
+ * The {@link SegmentInfo} describing the slow motion segment following (not including) the
+ * current frame, or null if there is no such segment.
+ */
+ @Nullable private SegmentInfo nextSegmentInfo;
+ /**
+ * The time delta to be added to the output timestamps before scaling to take the slow motion
+ * segments into account, in microseconds.
+ */
+ private long frameTimeDeltaUs;
+
+ public SefSlowMotionVideoSampleTransformer(Format format) {
+ scratch = new byte[NAL_START_CODE_LENGTH];
+ MetadataInfo metadataInfo = getMetadataInfo(format.metadata);
+ slowMotionData = metadataInfo.slowMotionData;
+ List segments =
+ slowMotionData != null ? slowMotionData.segments : ImmutableList.of();
+ segmentIterator = segments.iterator();
+ captureFrameRate = metadataInfo.captureFrameRate;
+ inputMaxLayer = metadataInfo.inputMaxLayer;
+ normalSpeedMaxLayer = metadataInfo.normalSpeedMaxLayer;
+ nextSegmentInfo =
+ segmentIterator.hasNext()
+ ? new SegmentInfo(segmentIterator.next(), inputMaxLayer, normalSpeedMaxLayer)
+ : null;
+ if (slowMotionData != null) {
+ checkArgument(
+ MimeTypes.VIDEO_H264.equals(format.sampleMimeType),
+ "Unsupported MIME type for SEF slow motion video track: " + format.sampleMimeType);
+ }
+ }
+
+ @Override
+ public void transformSample(DecoderInputBuffer buffer) {
+ if (slowMotionData == null) {
+ // The input is not an SEF slow motion video.
+ return;
+ }
+
+ ByteBuffer data = castNonNull(buffer.data);
+ int originalPosition = data.position();
+ data.position(originalPosition + NAL_START_CODE_LENGTH);
+ data.get(scratch, 0, 4); // Read nal_unit_header_svc_extension.
+ int nalUnitType = scratch[0] & 0x1F;
+ boolean svcExtensionFlag = ((scratch[1] & 0xFF) >> 7) == 1;
+ checkState(
+ nalUnitType == NAL_UNIT_TYPE_PREFIX && svcExtensionFlag,
+ "Missing SVC extension prefix NAL unit.");
+ int layer = (scratch[3] & 0xFF) >> 5;
+ boolean shouldKeepFrame = processCurrentFrame(layer, buffer.timeUs);
+ if (shouldKeepFrame) {
+ buffer.timeUs = getCurrentFrameOutputTimeUs(/* inputTimeUs= */ buffer.timeUs);
+ skipToNextNalUnit(data); // Skip over prefix_nal_unit_svc.
+ } else {
+ buffer.data = null;
+ }
+ }
+
+ /**
+ * Processes the current frame and returns whether it should be kept.
+ *
+ * @param layer The frame temporal SVC layer.
+ * @param timeUs The frame presentation time, in microseconds.
+ * @return Whether to keep the current frame.
+ */
+ @VisibleForTesting
+ /* package */ boolean processCurrentFrame(int layer, long timeUs) {
+ // Skip segments in the unlikely case that they do not contain any frame start time.
+ while (nextSegmentInfo != null && timeUs >= nextSegmentInfo.endTimeUs) {
+ enterNextSegment();
+ }
+
+ if (nextSegmentInfo != null && timeUs >= nextSegmentInfo.startTimeUs) {
+ enterNextSegment();
+ } else if (currentSegmentInfo != null && timeUs >= currentSegmentInfo.endTimeUs) {
+ leaveCurrentSegment();
+ }
+
+ int maxLayer = currentSegmentInfo != null ? currentSegmentInfo.maxLayer : normalSpeedMaxLayer;
+ return layer <= maxLayer || shouldKeepFrameForOutputValidity(layer, timeUs);
+ }
+
+ /** Updates the segments information so that the next segment becomes the current segment. */
+ private void enterNextSegment() {
+ if (currentSegmentInfo != null) {
+ leaveCurrentSegment();
+ }
+ currentSegmentInfo = nextSegmentInfo;
+ nextSegmentInfo =
+ segmentIterator.hasNext()
+ ? new SegmentInfo(segmentIterator.next(), inputMaxLayer, normalSpeedMaxLayer)
+ : null;
+ }
+
+ /**
+ * Updates the segments information so that there is no current segment. The next segment is
+ * unchanged.
+ */
+ @RequiresNonNull("currentSegmentInfo")
+ private void leaveCurrentSegment() {
+ frameTimeDeltaUs +=
+ (currentSegmentInfo.endTimeUs - currentSegmentInfo.startTimeUs)
+ * (currentSegmentInfo.speedDivisor - 1);
+ currentSegmentInfo = null;
+ }
+
+ /**
+ * Returns whether the frames of the next segment are based on the current frame. In this case,
+ * the current frame should be kept in order for the output to be valid.
+ *
+ * @param layer The frame temporal SVC layer.
+ * @param timeUs The frame presentation time, in microseconds.
+ * @return Whether to keep the current frame.
+ */
+ private boolean shouldKeepFrameForOutputValidity(int layer, long timeUs) {
+ if (nextSegmentInfo == null || layer >= nextSegmentInfo.maxLayer) {
+ return false;
+ }
+
+ long frameOffsetToSegmentEstimate =
+ (nextSegmentInfo.startTimeUs - timeUs) * INPUT_FRAME_RATE / C.MICROS_PER_SECOND;
+ float allowedError = 0.45f;
+ float baseMaxFrameOffsetToSegment =
+ -(1 << (inputMaxLayer - nextSegmentInfo.maxLayer)) + allowedError;
+ for (int i = 1; i < nextSegmentInfo.maxLayer; i++) {
+ if (frameOffsetToSegmentEstimate < (1 << (inputMaxLayer - i)) + baseMaxFrameOffsetToSegment) {
+ if (layer <= i) {
+ return true;
+ }
+ } else {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the time of the current frame in the output, in microseconds.
+ *
+ * This time is computed so that segments start and end at the correct times. As a result, the
+ * output frame rate might be variable.
+ *
+ *
This method can only be called if all the frames until the current one (included) have been
+ * {@link #processCurrentFrame(int, long) processed} in order, and if the next frames have not
+ * been processed yet.
+ */
+ @VisibleForTesting
+ /* package */ long getCurrentFrameOutputTimeUs(long inputTimeUs) {
+ long outputTimeUs = inputTimeUs + frameTimeDeltaUs;
+ if (currentSegmentInfo != null) {
+ outputTimeUs +=
+ (inputTimeUs - currentSegmentInfo.startTimeUs) * (currentSegmentInfo.speedDivisor - 1);
+ }
+ return Math.round(outputTimeUs * INPUT_FRAME_RATE / captureFrameRate);
+ }
+
+ /**
+ * Advances the position of {@code data} to the start of the next NAL unit.
+ *
+ * @throws IllegalStateException If no NAL unit is found.
+ */
+ private void skipToNextNalUnit(ByteBuffer data) {
+ int newPosition = data.position();
+ while (data.remaining() >= NAL_START_CODE_LENGTH) {
+ data.get(scratch, 0, NAL_START_CODE_LENGTH);
+ if (Arrays.equals(scratch, NAL_START_CODE)) {
+ data.position(newPosition);
+ return;
+ }
+ newPosition++;
+ data.position(newPosition);
+ }
+ throw new IllegalStateException("Could not find NAL unit start code.");
+ }
+
+ /** Returns the {@link MetadataInfo} derived from the {@link Metadata} provided. */
+ private static MetadataInfo getMetadataInfo(@Nullable Metadata metadata) {
+ MetadataInfo metadataInfo = new MetadataInfo();
+ if (metadata == null) {
+ return metadataInfo;
+ }
+
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof SmtaMetadataEntry) {
+ SmtaMetadataEntry smtaMetadataEntry = (SmtaMetadataEntry) entry;
+ metadataInfo.captureFrameRate = smtaMetadataEntry.captureFrameRate;
+ metadataInfo.inputMaxLayer = smtaMetadataEntry.svcTemporalLayerCount - 1;
+ } else if (entry instanceof SlowMotionData) {
+ metadataInfo.slowMotionData = (SlowMotionData) entry;
+ }
+ }
+
+ if (metadataInfo.slowMotionData == null) {
+ return metadataInfo;
+ }
+
+ checkState(metadataInfo.inputMaxLayer != C.INDEX_UNSET, "SVC temporal layer count not found.");
+ checkState(metadataInfo.captureFrameRate != C.RATE_UNSET, "Capture frame rate not found.");
+ checkState(
+ metadataInfo.captureFrameRate % 1 == 0
+ && metadataInfo.captureFrameRate % TARGET_OUTPUT_FRAME_RATE == 0,
+ "Invalid capture frame rate: " + metadataInfo.captureFrameRate);
+
+ int frameCountDivisor = (int) metadataInfo.captureFrameRate / TARGET_OUTPUT_FRAME_RATE;
+ int normalSpeedMaxLayer = metadataInfo.inputMaxLayer;
+ while (normalSpeedMaxLayer >= 0) {
+ if ((frameCountDivisor & 1) == 1) {
+ // Set normalSpeedMaxLayer only if captureFrameRate / TARGET_OUTPUT_FRAME_RATE is a power of
+ // 2. Otherwise, the target output frame rate cannot be reached because removing a layer
+ // divides the number of frames by 2.
+ checkState(
+ frameCountDivisor >> 1 == 0,
+ "Could not compute normal speed max SVC layer for capture frame rate "
+ + metadataInfo.captureFrameRate);
+ metadataInfo.normalSpeedMaxLayer = normalSpeedMaxLayer;
+ break;
+ }
+ frameCountDivisor >>= 1;
+ normalSpeedMaxLayer--;
+ }
+ return metadataInfo;
+ }
+
+ /** Metadata of an SEF slow motion input. */
+ private static final class MetadataInfo {
+ /**
+ * The frame rate at which the slow motion video has been captured in fps, or {@link
+ * C#RATE_UNSET} if it is unknown or invalid.
+ */
+ public float captureFrameRate;
+ /**
+ * The maximum SVC layer value of the input frames, or {@link C#INDEX_UNSET} if it is unknown.
+ */
+ public int inputMaxLayer;
+ /**
+ * The maximum SVC layer value of the frames to keep in order to play the video at normal speed
+ * at {@link #TARGET_OUTPUT_FRAME_RATE}, or {@link C#INDEX_UNSET} if it is unknown.
+ */
+ public int normalSpeedMaxLayer;
+ /** The input {@link SlowMotionData}. */
+ @Nullable public SlowMotionData slowMotionData;
+
+ public MetadataInfo() {
+ captureFrameRate = C.RATE_UNSET;
+ inputMaxLayer = C.INDEX_UNSET;
+ normalSpeedMaxLayer = C.INDEX_UNSET;
+ }
+ }
+
+ /** Information about a slow motion segment. */
+ private static final class SegmentInfo {
+ /** The segment start time, in microseconds. */
+ public final long startTimeUs;
+ /** The segment end time, in microseconds. */
+ public final long endTimeUs;
+ /**
+ * The segment speedDivisor.
+ *
+ * @see SlowMotionData.Segment#speedDivisor
+ */
+ public final int speedDivisor;
+ /**
+ * The maximum SVC layer value of the frames to keep in the segment in order to slow down the
+ * segment by {@code speedDivisor}.
+ */
+ public final int maxLayer;
+
+ public SegmentInfo(SlowMotionData.Segment segment, int inputMaxLayer, int normalSpeedLayer) {
+ this.startTimeUs = C.msToUs(segment.startTimeMs);
+ this.endTimeUs = C.msToUs(segment.endTimeMs);
+ this.speedDivisor = segment.speedDivisor;
+ this.maxLayer = getSlowMotionMaxLayer(speedDivisor, inputMaxLayer, normalSpeedLayer);
+ }
+
+ private static int getSlowMotionMaxLayer(
+ int speedDivisor, int inputMaxLayer, int normalSpeedMaxLayer) {
+ int maxLayer = normalSpeedMaxLayer;
+ // Increase the maximum layer to increase the number of frames in the segment. For every layer
+ // increment, the number of frames is doubled.
+ int shiftedSpeedDivisor = speedDivisor;
+ while (shiftedSpeedDivisor > 0) {
+ if ((shiftedSpeedDivisor & 1) == 1) {
+ checkState(shiftedSpeedDivisor >> 1 == 0, "Invalid speed divisor: " + speedDivisor);
+ break;
+ }
+ maxLayer++;
+ shiftedSpeedDivisor >>= 1;
+ }
+
+ // The optimal segment max layer can be larger than the input max layer. In this case, it is
+ // not possible to have speedDivisor times more frames in the segment than outside the
+ // segments. The desired speed must therefore be reached by keeping all the frames and by
+ // decreasing the frame rate in the segment.
+ return min(maxLayer, inputMaxLayer);
+ }
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SegmentSpeedProvider.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SegmentSpeedProvider.java
new file mode 100644
index 0000000000..2320367076
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SegmentSpeedProvider.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.metadata.mp4.SlowMotionData.Segment.BY_START_THEN_END_THEN_DIVISOR;
+import static com.google.android.exoplayer2.util.Assertions.checkArgument;
+
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
+import com.google.android.exoplayer2.metadata.mp4.SlowMotionData.Segment;
+import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** A {@link SpeedProvider} for slow motion segments. */
+/* package */ class SegmentSpeedProvider implements SpeedProvider {
+
+ /**
+ * Input frame rate of Samsung Slow motion videos is always 30. See
+ * go/exoplayer-sef-slomo-video-flattening.
+ */
+ private static final int INPUT_FRAME_RATE = 30;
+
+ private final ImmutableSortedMap speedsByStartTimeUs;
+ private final float baseSpeedMultiplier;
+
+ public SegmentSpeedProvider(Format format) {
+ float captureFrameRate = getCaptureFrameRate(format);
+ this.baseSpeedMultiplier =
+ captureFrameRate == C.RATE_UNSET ? 1 : captureFrameRate / INPUT_FRAME_RATE;
+ this.speedsByStartTimeUs = buildSpeedByStartTimeUsMap(format, baseSpeedMultiplier);
+ }
+
+ @Override
+ public float getSpeed(long timeUs) {
+ checkArgument(timeUs >= 0);
+ @Nullable Map.Entry entry = speedsByStartTimeUs.floorEntry(timeUs);
+ return entry != null ? entry.getValue() : baseSpeedMultiplier;
+ }
+
+ private static ImmutableSortedMap buildSpeedByStartTimeUsMap(
+ Format format, float baseSpeed) {
+ List segments = extractSlowMotionSegments(format);
+
+ if (segments.isEmpty()) {
+ return ImmutableSortedMap.of();
+ }
+
+ TreeMap speedsByStartTimeUs = new TreeMap<>();
+
+ // Start time maps to the segment speed.
+ for (int i = 0; i < segments.size(); i++) {
+ Segment currentSegment = segments.get(i);
+ speedsByStartTimeUs.put(
+ C.msToUs(currentSegment.startTimeMs), baseSpeed / currentSegment.speedDivisor);
+ }
+
+ // If the map has an entry at endTime, this is the next segments start time. If no such entry
+ // exists, map the endTime to base speed because the times after the end time are not in a
+ // segment.
+ for (int i = 0; i < segments.size(); i++) {
+ Segment currentSegment = segments.get(i);
+ if (!speedsByStartTimeUs.containsKey(C.msToUs(currentSegment.endTimeMs))) {
+ speedsByStartTimeUs.put(C.msToUs(currentSegment.endTimeMs), baseSpeed);
+ }
+ }
+
+ return ImmutableSortedMap.copyOf(speedsByStartTimeUs);
+ }
+
+ private static float getCaptureFrameRate(Format format) {
+ @Nullable Metadata metadata = format.metadata;
+ if (metadata == null) {
+ return C.RATE_UNSET;
+ }
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof SmtaMetadataEntry) {
+ return ((SmtaMetadataEntry) entry).captureFrameRate;
+ }
+ }
+
+ return C.RATE_UNSET;
+ }
+
+ private static ImmutableList extractSlowMotionSegments(Format format) {
+ List segments = new ArrayList<>();
+ @Nullable Metadata metadata = format.metadata;
+ if (metadata != null) {
+ for (int i = 0; i < metadata.length(); i++) {
+ Metadata.Entry entry = metadata.get(i);
+ if (entry instanceof SlowMotionData) {
+ segments.addAll(((SlowMotionData) entry).segments);
+ }
+ }
+ }
+ return ImmutableList.sortedCopyOf(BY_START_THEN_END_THEN_DIVISOR, segments);
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java
new file mode 100644
index 0000000000..f8109e031c
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/SpeedProvider.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+/** A custom interface that determines the speed for media at specific timestamps. */
+public interface SpeedProvider {
+
+ /**
+ * Provides the speed that the media should be played at, based on the timeUs.
+ *
+ * @param timeUs The timestamp of the media.
+ * @return The speed that the media should be played at, based on the timeUs.
+ */
+ float getSpeed(long timeUs);
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java
new file mode 100644
index 0000000000..b0c9e8d2cc
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformation.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+/** A media transformation configuration. */
+/* package */ final class Transformation {
+
+ public final boolean removeAudio;
+ public final boolean removeVideo;
+ public final boolean flattenForSlowMotion;
+ public final String outputMimeType;
+
+ public Transformation(
+ boolean removeAudio,
+ boolean removeVideo,
+ boolean flattenForSlowMotion,
+ String outputMimeType) {
+ this.removeAudio = removeAudio;
+ this.removeVideo = removeVideo;
+ this.flattenForSlowMotion = flattenForSlowMotion;
+ this.outputMimeType = outputMimeType;
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java
new file mode 100644
index 0000000000..1ca5be3570
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java
@@ -0,0 +1,662 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
+import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
+import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
+import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.DefaultLoadControl;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.analytics.AnalyticsListener;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
+import com.google.android.exoplayer2.metadata.MetadataOutput;
+import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
+import com.google.android.exoplayer2.source.MediaSourceFactory;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.TextOutput;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * A transformer to transform media inputs.
+ *
+ * The same Transformer instance can be used to transform multiple inputs (sequentially, not
+ * concurrently).
+ *
+ *
Transformer instances must be accessed from a single application thread. For the vast majority
+ * of cases this should be the application's main thread. The thread on which a Transformer instance
+ * must be accessed can be explicitly specified by passing a {@link Looper} when creating the
+ * transformer. If no Looper is specified, then the Looper of the thread that the {@link
+ * Transformer.Builder} is created on is used, or if that thread does not have a Looper, the Looper
+ * of the application's main thread is used. In all cases the Looper of the thread from which the
+ * transformer must be accessed can be queried using {@link #getApplicationLooper()}.
+ */
+@RequiresApi(18)
+public final class Transformer {
+
+ /** A builder for {@link Transformer} instances. */
+ public static final class Builder {
+
+ private @MonotonicNonNull Context context;
+ private @MonotonicNonNull MediaSourceFactory mediaSourceFactory;
+ private Muxer.Factory muxerFactory;
+ private boolean removeAudio;
+ private boolean removeVideo;
+ private boolean flattenForSlowMotion;
+ private String outputMimeType;
+ private Transformer.Listener listener;
+ private Looper looper;
+ private Clock clock;
+
+ /** Creates a builder with default values. */
+ public Builder() {
+ muxerFactory = new FrameworkMuxer.Factory();
+ outputMimeType = MimeTypes.VIDEO_MP4;
+ listener = new Listener() {};
+ looper = Util.getCurrentOrMainLooper();
+ clock = Clock.DEFAULT;
+ }
+
+ /** Creates a builder with the values of the provided {@link Transformer}. */
+ private Builder(Transformer transformer) {
+ this.context = transformer.context;
+ this.mediaSourceFactory = transformer.mediaSourceFactory;
+ this.muxerFactory = transformer.muxerFactory;
+ this.removeAudio = transformer.transformation.removeAudio;
+ this.removeVideo = transformer.transformation.removeVideo;
+ this.flattenForSlowMotion = transformer.transformation.flattenForSlowMotion;
+ this.outputMimeType = transformer.transformation.outputMimeType;
+ this.listener = transformer.listener;
+ this.looper = transformer.looper;
+ this.clock = transformer.clock;
+ }
+
+ /**
+ * Sets the {@link Context}.
+ *
+ *
This parameter is mandatory.
+ *
+ * @param context The {@link Context}.
+ * @return This builder.
+ */
+ public Builder setContext(Context context) {
+ this.context = context.getApplicationContext();
+ return this;
+ }
+
+ /**
+ * Sets the {@link MediaSourceFactory} to be used to retrieve the inputs to transform. The
+ * default value is a {@link DefaultMediaSourceFactory} built with the context provided in
+ * {@link #setContext(Context)}.
+ *
+ * @param mediaSourceFactory A {@link MediaSourceFactory}.
+ * @return This builder.
+ */
+ public Builder setMediaSourceFactory(MediaSourceFactory mediaSourceFactory) {
+ this.mediaSourceFactory = mediaSourceFactory;
+ return this;
+ }
+
+ /**
+ * Sets whether to remove the audio from the output. The default value is {@code false}.
+ *
+ *
The audio and video cannot both be removed because the output would not contain any
+ * samples.
+ *
+ * @param removeAudio Whether to remove the audio.
+ * @return This builder.
+ */
+ public Builder setRemoveAudio(boolean removeAudio) {
+ this.removeAudio = removeAudio;
+ return this;
+ }
+
+ /**
+ * Sets whether to remove the video from the output. The default value is {@code false}.
+ *
+ *
The audio and video cannot both be removed because the output would not contain any
+ * samples.
+ *
+ * @param removeVideo Whether to remove the video.
+ * @return This builder.
+ */
+ public Builder setRemoveVideo(boolean removeVideo) {
+ this.removeVideo = removeVideo;
+ return this;
+ }
+
+ /**
+ * Sets whether the input should be flattened for media containing slow motion markers. The
+ * transformed output is obtained by removing the slow motion metadata and by actually slowing
+ * down the parts of the video and audio streams defined in this metadata. The default value for
+ * {@code flattenForSlowMotion} is {@code false}.
+ *
+ *
Only Samsung Extension Format (SEF) slow motion metadata type is supported. The
+ * transformation has no effect if the input does not contain this metadata type.
+ *
+ *
For SEF slow motion media, the following assumptions are made on the input:
+ *
+ *
+ * - The input container format is (unfragmented) MP4.
+ *
- The input contains an AVC video elementary stream with temporal SVC.
+ *
- The recording frame rate of the video is 120 or 240 fps.
+ *
+ *
+ * If specifying a {@link MediaSourceFactory} using {@link
+ * #setMediaSourceFactory(MediaSourceFactory)}, make sure that {@link
+ * Mp4Extractor#FLAG_READ_SEF_DATA} is set on the {@link Mp4Extractor} used. Otherwise, the slow
+ * motion metadata will be ignored and the input won't be flattened.
+ *
+ * @param flattenForSlowMotion Whether to flatten for slow motion.
+ * @return This builder.
+ */
+ public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
+ this.flattenForSlowMotion = flattenForSlowMotion;
+ return this;
+ }
+
+ /**
+ * Sets the MIME type of the output. The default value is {@link MimeTypes#VIDEO_MP4}. Supported
+ * values are:
+ *
+ *
+ * - {@link MimeTypes#VIDEO_MP4}
+ *
- {@link MimeTypes#VIDEO_WEBM} from API level 21
+ *
+ *
+ * @param outputMimeType The MIME type of the output.
+ * @return This builder.
+ */
+ public Builder setOutputMimeType(String outputMimeType) {
+ this.outputMimeType = outputMimeType;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Transformer.Listener} to listen to the transformation events.
+ *
+ * This is equivalent to {@link Transformer#setListener(Listener)}.
+ *
+ * @param listener A {@link Transformer.Listener}.
+ * @return This builder.
+ */
+ public Builder setListener(Transformer.Listener listener) {
+ this.listener = listener;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Looper} that must be used for all calls to the transformer and that is used
+ * to call listeners on. The default value is the Looper of the thread that this builder was
+ * created on, or if that thread does not have a Looper, the Looper of the application's main
+ * thread.
+ *
+ * @param looper A {@link Looper}.
+ * @return This builder.
+ */
+ public Builder setLooper(Looper looper) {
+ this.looper = looper;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Clock} that will be used by the transformer. The default value is {@link
+ * Clock#DEFAULT}.
+ *
+ * @param clock The {@link Clock} instance.
+ * @return This builder.
+ */
+ @VisibleForTesting
+ /* package */ Builder setClock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ /**
+ * Sets the factory for muxers that write the media container.
+ *
+ * @param muxerFactory A {@link Muxer.Factory}.
+ * @return This builder.
+ */
+ @VisibleForTesting
+ /* package */ Builder setMuxerFactory(Muxer.Factory muxerFactory) {
+ this.muxerFactory = muxerFactory;
+ return this;
+ }
+
+ /**
+ * Builds a {@link Transformer} instance.
+ *
+ * @throws IllegalStateException If the {@link Context} has not been provided.
+ * @throws IllegalStateException If both audio and video have been removed (otherwise the output
+ * would not contain any samples).
+ * @throws IllegalStateException If the muxer doesn't support the requested output MIME type.
+ */
+ public Transformer build() {
+ checkStateNotNull(context);
+ if (mediaSourceFactory == null) {
+ DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
+ if (flattenForSlowMotion) {
+ defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA);
+ }
+ mediaSourceFactory = new DefaultMediaSourceFactory(context, defaultExtractorsFactory);
+ }
+ checkState(
+ muxerFactory.supportsOutputMimeType(outputMimeType),
+ "Unsupported output MIME type: " + outputMimeType);
+ Transformation transformation =
+ new Transformation(removeAudio, removeVideo, flattenForSlowMotion, outputMimeType);
+ return new Transformer(
+ context, mediaSourceFactory, muxerFactory, transformation, listener, looper, clock);
+ }
+ }
+
+ /** A listener for the transformation events. */
+ public interface Listener {
+
+ /**
+ * Called when the transformation is completed.
+ *
+ * @param inputMediaItem The {@link MediaItem} for which the transformation is completed.
+ */
+ default void onTransformationCompleted(MediaItem inputMediaItem) {}
+
+ /**
+ * Called if an error occurs during the transformation.
+ *
+ * @param inputMediaItem The {@link MediaItem} for which the error occurs.
+ * @param exception The exception describing the error.
+ */
+ default void onTransformationError(MediaItem inputMediaItem, Exception exception) {}
+ }
+
+ /**
+ * Progress state. One of {@link #PROGRESS_STATE_WAITING_FOR_AVAILABILITY}, {@link
+ * #PROGRESS_STATE_AVAILABLE}, {@link #PROGRESS_STATE_UNAVAILABLE}, {@link
+ * #PROGRESS_STATE_NO_TRANSFORMATION}
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ PROGRESS_STATE_WAITING_FOR_AVAILABILITY,
+ PROGRESS_STATE_AVAILABLE,
+ PROGRESS_STATE_UNAVAILABLE,
+ PROGRESS_STATE_NO_TRANSFORMATION
+ })
+ public @interface ProgressState {}
+
+ /**
+ * Indicates that the progress is unavailable for the current transformation, but might become
+ * available.
+ */
+ public static final int PROGRESS_STATE_WAITING_FOR_AVAILABILITY = 0;
+ /** Indicates that the progress is available. */
+ public static final int PROGRESS_STATE_AVAILABLE = 1;
+ /** Indicates that the progress is permanently unavailable for the current transformation. */
+ public static final int PROGRESS_STATE_UNAVAILABLE = 2;
+ /** Indicates that there is no current transformation. */
+ public static final int PROGRESS_STATE_NO_TRANSFORMATION = 4;
+
+ private final Context context;
+ private final MediaSourceFactory mediaSourceFactory;
+ private final Muxer.Factory muxerFactory;
+ private final Transformation transformation;
+ private final Looper looper;
+ private final Clock clock;
+
+ private Transformer.Listener listener;
+ @Nullable private MuxerWrapper muxerWrapper;
+ @Nullable private SimpleExoPlayer player;
+ @ProgressState private int progressState;
+
+ private Transformer(
+ Context context,
+ MediaSourceFactory mediaSourceFactory,
+ Muxer.Factory muxerFactory,
+ Transformation transformation,
+ Transformer.Listener listener,
+ Looper looper,
+ Clock clock) {
+ checkState(
+ !transformation.removeAudio || !transformation.removeVideo,
+ "Audio and video cannot both be removed.");
+ this.context = context;
+ this.mediaSourceFactory = mediaSourceFactory;
+ this.muxerFactory = muxerFactory;
+ this.transformation = transformation;
+ this.listener = listener;
+ this.looper = looper;
+ this.clock = clock;
+ progressState = PROGRESS_STATE_NO_TRANSFORMATION;
+ }
+
+ /** Returns a {@link Transformer.Builder} initialized with the values of this instance. */
+ public Builder buildUpon() {
+ return new Builder(this);
+ }
+
+ /**
+ * Sets the {@link Transformer.Listener} to listen to the transformation events.
+ *
+ * @param listener A {@link Transformer.Listener}.
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ */
+ public void setListener(Transformer.Listener listener) {
+ verifyApplicationThread();
+ this.listener = listener;
+ }
+
+ /**
+ * Starts an asynchronous operation to transform the given {@link MediaItem}.
+ *
+ *
The transformation state is notified through the {@link Builder#setListener(Listener)
+ * listener}.
+ *
+ *
Concurrent transformations on the same Transformer object are not allowed.
+ *
+ *
The output can contain at most one video track and one audio track. Other track types are
+ * ignored. For adaptive bitrate {@link com.google.android.exoplayer2.source.MediaSource media
+ * sources}, the highest bitrate video and audio streams are selected.
+ *
+ * @param mediaItem The {@link MediaItem} to transform. The supported sample formats depend on the
+ * output container format and are described in {@link MediaMuxer#addTrack(MediaFormat)}.
+ * @param path The path to the output file.
+ * @throws IllegalArgumentException If the path is invalid.
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ * @throws IllegalStateException If a transformation is already in progress.
+ * @throws IOException If an error occurs opening the output file for writing.
+ */
+ public void startTransformation(MediaItem mediaItem, String path) throws IOException {
+ startTransformation(mediaItem, muxerFactory.create(path, transformation.outputMimeType));
+ }
+
+ /**
+ * Starts an asynchronous operation to transform the given {@link MediaItem}.
+ *
+ *
The transformation state is notified through the {@link Builder#setListener(Listener)
+ * listener}.
+ *
+ *
Concurrent transformations on the same Transformer object are not allowed.
+ *
+ *
The output can contain at most one video track and one audio track. Other track types are
+ * ignored. For adaptive bitrate {@link com.google.android.exoplayer2.source.MediaSource media
+ * sources}, the highest bitrate video and audio streams are selected.
+ *
+ * @param mediaItem The {@link MediaItem} to transform. The supported sample formats depend on the
+ * output container format and are described in {@link MediaMuxer#addTrack(MediaFormat)}.
+ * @param parcelFileDescriptor A readable and writable {@link ParcelFileDescriptor} of the output.
+ * The file referenced by this ParcelFileDescriptor should not be used before the
+ * transformation is completed. It is the responsibility of the caller to close the
+ * ParcelFileDescriptor. This can be done after this method returns.
+ * @throws IllegalArgumentException If the file descriptor is invalid.
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ * @throws IllegalStateException If a transformation is already in progress.
+ * @throws IOException If an error occurs opening the output file for writing.
+ */
+ @RequiresApi(26)
+ public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor)
+ throws IOException {
+ startTransformation(
+ mediaItem, muxerFactory.create(parcelFileDescriptor, transformation.outputMimeType));
+ }
+
+ private void startTransformation(MediaItem mediaItem, Muxer muxer) {
+ verifyApplicationThread();
+ if (player != null) {
+ throw new IllegalStateException("There is already a transformation in progress.");
+ }
+
+ MuxerWrapper muxerWrapper = new MuxerWrapper(muxer);
+ this.muxerWrapper = muxerWrapper;
+ DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
+ trackSelector.setParameters(
+ new DefaultTrackSelector.ParametersBuilder(context)
+ .setForceHighestSupportedBitrate(true)
+ .build());
+ // Arbitrarily decrease buffers for playback so that samples start being sent earlier to the
+ // muxer (rebuffers are less problematic for the transformation use case).
+ DefaultLoadControl loadControl =
+ new DefaultLoadControl.Builder()
+ .setBufferDurationsMs(
+ DEFAULT_MIN_BUFFER_MS,
+ DEFAULT_MAX_BUFFER_MS,
+ DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
+ DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
+ .build();
+ player =
+ new SimpleExoPlayer.Builder(
+ context, new TransformerRenderersFactory(muxerWrapper, transformation))
+ .setMediaSourceFactory(mediaSourceFactory)
+ .setTrackSelector(trackSelector)
+ .setLoadControl(loadControl)
+ .setLooper(looper)
+ .setClock(clock)
+ .build();
+ player.setMediaItem(mediaItem);
+ player.addAnalyticsListener(new TransformerAnalyticsListener(mediaItem, muxerWrapper));
+ player.prepare();
+
+ progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
+ }
+
+ /**
+ * Returns the {@link Looper} associated with the application thread that's used to access the
+ * transformer and on which transformer events are received.
+ */
+ public Looper getApplicationLooper() {
+ return looper;
+ }
+
+ /**
+ * Returns the current {@link ProgressState} and updates {@code progressHolder} with the current
+ * progress if it is {@link #PROGRESS_STATE_AVAILABLE available}.
+ *
+ *
After a transformation {@link Listener#onTransformationCompleted(MediaItem) completes}, this
+ * method returns {@link #PROGRESS_STATE_NO_TRANSFORMATION}.
+ *
+ * @param progressHolder A {@link ProgressHolder}, updated to hold the percentage progress if
+ * {@link #PROGRESS_STATE_AVAILABLE available}.
+ * @return The {@link ProgressState}.
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ */
+ @ProgressState
+ public int getProgress(ProgressHolder progressHolder) {
+ verifyApplicationThread();
+ if (progressState == PROGRESS_STATE_AVAILABLE) {
+ Player player = checkNotNull(this.player);
+ long durationMs = player.getDuration();
+ long positionMs = player.getCurrentPosition();
+ progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
+ }
+ return progressState;
+ }
+
+ /**
+ * Cancels the transformation that is currently in progress, if any.
+ *
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ */
+ public void cancel() {
+ releaseResources(/* forCancellation= */ true);
+ }
+
+ /**
+ * Releases the resources.
+ *
+ * @param forCancellation Whether the reason for releasing the resources is the transformation
+ * cancellation.
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ * @throws IllegalStateException If the muxer is in the wrong state and {@code forCancellation} is
+ * false.
+ */
+ private void releaseResources(boolean forCancellation) {
+ verifyApplicationThread();
+ if (player != null) {
+ player.release();
+ player = null;
+ }
+ if (muxerWrapper != null) {
+ muxerWrapper.release(forCancellation);
+ muxerWrapper = null;
+ }
+ progressState = PROGRESS_STATE_NO_TRANSFORMATION;
+ }
+
+ private void verifyApplicationThread() {
+ if (Looper.myLooper() != looper) {
+ throw new IllegalStateException("Transformer is accessed on the wrong thread.");
+ }
+ }
+
+ private static final class TransformerRenderersFactory implements RenderersFactory {
+
+ private final MuxerWrapper muxerWrapper;
+ private final TransformerMediaClock mediaClock;
+ private final Transformation transformation;
+
+ public TransformerRenderersFactory(MuxerWrapper muxerWrapper, Transformation transformation) {
+ this.muxerWrapper = muxerWrapper;
+ this.transformation = transformation;
+ mediaClock = new TransformerMediaClock();
+ }
+
+ @Override
+ public Renderer[] createRenderers(
+ Handler eventHandler,
+ VideoRendererEventListener videoRendererEventListener,
+ AudioRendererEventListener audioRendererEventListener,
+ TextOutput textRendererOutput,
+ MetadataOutput metadataRendererOutput) {
+ int rendererCount = transformation.removeAudio || transformation.removeVideo ? 1 : 2;
+ Renderer[] renderers = new Renderer[rendererCount];
+ int index = 0;
+ if (!transformation.removeAudio) {
+ renderers[index] = new TransformerAudioRenderer(muxerWrapper, mediaClock, transformation);
+ index++;
+ }
+ if (!transformation.removeVideo) {
+ renderers[index] = new TransformerVideoRenderer(muxerWrapper, mediaClock, transformation);
+ index++;
+ }
+ return renderers;
+ }
+ }
+
+ private final class TransformerAnalyticsListener implements AnalyticsListener {
+
+ private final MediaItem mediaItem;
+ private final MuxerWrapper muxerWrapper;
+
+ public TransformerAnalyticsListener(MediaItem mediaItem, MuxerWrapper muxerWrapper) {
+ this.mediaItem = mediaItem;
+ this.muxerWrapper = muxerWrapper;
+ }
+
+ @Override
+ public void onPlaybackStateChanged(EventTime eventTime, int state) {
+ if (state == Player.STATE_ENDED) {
+ handleTransformationEnded(/* exception= */ null);
+ }
+ }
+
+ @Override
+ public void onTimelineChanged(EventTime eventTime, int reason) {
+ if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) {
+ return;
+ }
+ Timeline.Window window = new Timeline.Window();
+ eventTime.timeline.getWindow(/* windowIndex= */ 0, window);
+ if (!window.isPlaceholder) {
+ long durationUs = window.durationUs;
+ // Make progress permanently unavailable if the duration is unknown, so that it doesn't jump
+ // to a high value at the end of the transformation if the duration is set once the media is
+ // entirely loaded.
+ progressState =
+ durationUs <= 0 || durationUs == C.TIME_UNSET
+ ? PROGRESS_STATE_UNAVAILABLE
+ : PROGRESS_STATE_AVAILABLE;
+ checkNotNull(player).play();
+ }
+ }
+
+ @Override
+ public void onTracksChanged(
+ EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ if (muxerWrapper.getTrackCount() == 0) {
+ handleTransformationEnded(
+ new IllegalStateException(
+ "The output does not contain any tracks. Check that at least one of the input"
+ + " sample formats is supported."));
+ }
+ }
+
+ @Override
+ public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
+ handleTransformationEnded(error);
+ }
+
+ private void handleTransformationEnded(@Nullable Exception exception) {
+ try {
+ releaseResources(/* forCancellation= */ false);
+ } catch (IllegalStateException e) {
+ if (exception == null) {
+ exception = e;
+ }
+ }
+
+ if (exception == null) {
+ listener.onTransformationCompleted(mediaItem);
+ } else {
+ listener.onTransformationError(mediaItem, exception);
+ }
+ }
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java
new file mode 100644
index 0000000000..627120acb4
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerAudioRenderer.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static java.lang.Math.min;
+
+import android.media.MediaCodec.BufferInfo;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.audio.AudioProcessor;
+import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat;
+import com.google.android.exoplayer2.audio.SonicAudioProcessor;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+@RequiresApi(18)
+/* package */ final class TransformerAudioRenderer extends TransformerBaseRenderer {
+
+ private static final String TAG = "TransformerAudioRenderer";
+ private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024;
+ private static final float SPEED_UNSET = -1f;
+
+ private final DecoderInputBuffer decoderInputBuffer;
+ private final DecoderInputBuffer encoderInputBuffer;
+ private final SonicAudioProcessor sonicAudioProcessor;
+
+ @Nullable private MediaCodecAdapterWrapper decoder;
+ @Nullable private MediaCodecAdapterWrapper encoder;
+ @Nullable private SpeedProvider speedProvider;
+ @Nullable private Format inputFormat;
+ @Nullable private AudioFormat encoderInputAudioFormat;
+
+ private ByteBuffer sonicOutputBuffer;
+ private long nextEncoderInputBufferTimeUs;
+ private float currentSpeed;
+ private boolean muxerWrapperTrackEnded;
+ private boolean hasEncoderOutputFormat;
+ private boolean drainingSonicForSpeedChange;
+
+ public TransformerAudioRenderer(
+ MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) {
+ super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation);
+ decoderInputBuffer =
+ new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
+ encoderInputBuffer =
+ new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
+ sonicAudioProcessor = new SonicAudioProcessor();
+ sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER;
+ nextEncoderInputBufferTimeUs = 0;
+ currentSpeed = SPEED_UNSET;
+ }
+
+ @Override
+ public String getName() {
+ return TAG;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return muxerWrapperTrackEnded;
+ }
+
+ @Override
+ protected void onReset() {
+ decoderInputBuffer.clear();
+ decoderInputBuffer.data = null;
+ encoderInputBuffer.clear();
+ encoderInputBuffer.data = null;
+ sonicAudioProcessor.reset();
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ }
+ if (encoder != null) {
+ encoder.release();
+ encoder = null;
+ }
+ speedProvider = null;
+ inputFormat = null;
+ encoderInputAudioFormat = null;
+ sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER;
+ nextEncoderInputBufferTimeUs = 0;
+ currentSpeed = SPEED_UNSET;
+ muxerWrapperTrackEnded = false;
+ hasEncoderOutputFormat = false;
+ drainingSonicForSpeedChange = false;
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (!isRendererStarted || isEnded()) {
+ return;
+ }
+
+ if (ensureDecoderConfigured()) {
+ if (ensureEncoderAndAudioProcessingConfigured()) {
+ while (drainEncoderToFeedMuxer()) {}
+ if (sonicAudioProcessor.isActive()) {
+ while (drainSonicToFeedEncoder()) {}
+ while (drainDecoderToFeedSonic()) {}
+ } else {
+ while (drainDecoderToFeedEncoder()) {}
+ }
+ }
+ while (feedDecoderInputFromSource()) {}
+ }
+ }
+
+ /**
+ * Attempts to write encoder output data to the muxer, and returns whether it may be possible to
+ * write more data immediately by calling this method again.
+ */
+ private boolean drainEncoderToFeedMuxer() {
+ MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder);
+ if (!hasEncoderOutputFormat) {
+ @Nullable Format encoderOutputFormat = encoder.getOutputFormat();
+ if (encoderOutputFormat == null) {
+ return false;
+ }
+ hasEncoderOutputFormat = true;
+ muxerWrapper.addTrackFormat(encoderOutputFormat);
+ }
+
+ if (encoder.isEnded()) {
+ muxerWrapper.endTrack(getTrackType());
+ muxerWrapperTrackEnded = true;
+ return false;
+ }
+ @Nullable ByteBuffer encoderOutputBuffer = encoder.getOutputBuffer();
+ if (encoderOutputBuffer == null) {
+ return false;
+ }
+ BufferInfo encoderOutputBufferInfo = checkNotNull(encoder.getOutputBufferInfo());
+ if (!muxerWrapper.writeSample(
+ getTrackType(),
+ encoderOutputBuffer,
+ /* isKeyFrame= */ true,
+ encoderOutputBufferInfo.presentationTimeUs)) {
+ return false;
+ }
+ encoder.releaseOutputBuffer();
+ return true;
+ }
+
+ /**
+ * Attempts to pass decoder output data to the encoder, and returns whether it may be possible to
+ * pass more data immediately by calling this method again.
+ */
+ private boolean drainDecoderToFeedEncoder() {
+ MediaCodecAdapterWrapper decoder = checkNotNull(this.decoder);
+ MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder);
+ if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) {
+ return false;
+ }
+
+ if (decoder.isEnded()) {
+ queueEndOfStreamToEncoder();
+ return false;
+ }
+
+ @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer();
+ if (decoderOutputBuffer == null) {
+ return false;
+ }
+ if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) {
+ flushSonicAndSetSpeed(currentSpeed);
+ return false;
+ }
+ feedEncoder(decoderOutputBuffer);
+ if (!decoderOutputBuffer.hasRemaining()) {
+ decoder.releaseOutputBuffer();
+ }
+ return true;
+ }
+
+ /**
+ * Attempts to pass audio processor output data to the encoder, and returns whether it may be
+ * possible to pass more data immediately by calling this method again.
+ */
+ private boolean drainSonicToFeedEncoder() {
+ MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder);
+ if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) {
+ return false;
+ }
+
+ if (!sonicOutputBuffer.hasRemaining()) {
+ sonicOutputBuffer = sonicAudioProcessor.getOutput();
+ if (!sonicOutputBuffer.hasRemaining()) {
+ if (checkNotNull(decoder).isEnded() && sonicAudioProcessor.isEnded()) {
+ queueEndOfStreamToEncoder();
+ }
+ return false;
+ }
+ }
+
+ feedEncoder(sonicOutputBuffer);
+ return true;
+ }
+
+ /**
+ * Attempts to process decoder output audio, and returns whether it may be possible to process
+ * more data immediately by calling this method again.
+ */
+ private boolean drainDecoderToFeedSonic() {
+ MediaCodecAdapterWrapper decoder = checkNotNull(this.decoder);
+
+ if (drainingSonicForSpeedChange) {
+ if (!sonicAudioProcessor.isEnded()) {
+ // Sonic needs draining, but has not fully drained yet.
+ return false;
+ }
+ flushSonicAndSetSpeed(currentSpeed);
+ drainingSonicForSpeedChange = false;
+ }
+
+ // Sonic invalidates any previous output buffer when more input is queued, so we don't queue if
+ // there is output still to be processed.
+ if (sonicOutputBuffer.hasRemaining()) {
+ return false;
+ }
+
+ if (decoder.isEnded()) {
+ sonicAudioProcessor.queueEndOfStream();
+ return false;
+ }
+ checkState(!sonicAudioProcessor.isEnded());
+
+ @Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer();
+ if (decoderOutputBuffer == null) {
+ return false;
+ }
+ if (isSpeedChanging(checkNotNull(decoder.getOutputBufferInfo()))) {
+ sonicAudioProcessor.queueEndOfStream();
+ drainingSonicForSpeedChange = true;
+ return false;
+ }
+ sonicAudioProcessor.queueInput(decoderOutputBuffer);
+ if (!decoderOutputBuffer.hasRemaining()) {
+ decoder.releaseOutputBuffer();
+ }
+ return true;
+ }
+
+ /**
+ * Attempts to pass input data to the decoder, and returns whether it may be possible to pass more
+ * data immediately by calling this method again.
+ */
+ private boolean feedDecoderInputFromSource() {
+ MediaCodecAdapterWrapper decoder = checkNotNull(this.decoder);
+ if (!decoder.maybeDequeueInputBuffer(decoderInputBuffer)) {
+ return false;
+ }
+
+ decoderInputBuffer.clear();
+ @SampleStream.ReadDataResult
+ int result = readSource(getFormatHolder(), decoderInputBuffer, /* formatRequired= */ false);
+ switch (result) {
+ case C.RESULT_BUFFER_READ:
+ mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs);
+ decoderInputBuffer.flip();
+ decoder.queueInputBuffer(decoderInputBuffer);
+ return !decoderInputBuffer.isEndOfStream();
+ case C.RESULT_FORMAT_READ:
+ throw new IllegalStateException("Format changes are not supported.");
+ case C.RESULT_NOTHING_READ:
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Feeds as much data as possible between the current position and limit of the specified {@link
+ * ByteBuffer} to the encoder, and advances its position by the number of bytes fed.
+ */
+ private void feedEncoder(ByteBuffer inputBuffer) {
+ AudioFormat encoderInputAudioFormat = checkNotNull(this.encoderInputAudioFormat);
+ MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder);
+ ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data);
+ int bufferLimit = inputBuffer.limit();
+ inputBuffer.limit(min(bufferLimit, inputBuffer.position() + encoderInputBufferData.capacity()));
+ encoderInputBufferData.put(inputBuffer);
+ encoderInputBuffer.timeUs = nextEncoderInputBufferTimeUs;
+ nextEncoderInputBufferTimeUs +=
+ getBufferDurationUs(
+ /* bytesWritten= */ encoderInputBufferData.position(),
+ encoderInputAudioFormat.bytesPerFrame,
+ encoderInputAudioFormat.sampleRate);
+ encoderInputBuffer.setFlags(0);
+ encoderInputBuffer.flip();
+ inputBuffer.limit(bufferLimit);
+ encoder.queueInputBuffer(encoderInputBuffer);
+ }
+
+ private void queueEndOfStreamToEncoder() {
+ MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder);
+ checkState(checkNotNull(encoderInputBuffer.data).position() == 0);
+ encoderInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ encoderInputBuffer.flip();
+ // Queuing EOS should only occur with an empty buffer.
+ encoder.queueInputBuffer(encoderInputBuffer);
+ }
+
+ /**
+ * Attempts to configure the {@link #encoder} and Sonic (if applicable), if they have not been
+ * configured yet, and returns whether they have been configured.
+ */
+ private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException {
+ if (encoder != null) {
+ return true;
+ }
+ MediaCodecAdapterWrapper decoder = checkNotNull(this.decoder);
+ @Nullable Format decoderOutputFormat = decoder.getOutputFormat();
+ if (decoderOutputFormat == null) {
+ return false;
+ }
+ AudioFormat outputAudioFormat =
+ new AudioFormat(
+ decoderOutputFormat.sampleRate,
+ decoderOutputFormat.channelCount,
+ decoderOutputFormat.pcmEncoding);
+ if (transformation.flattenForSlowMotion) {
+ try {
+ outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat);
+ flushSonicAndSetSpeed(currentSpeed);
+ } catch (AudioProcessor.UnhandledAudioFormatException e) {
+ throw createRendererException(e);
+ }
+ }
+ try {
+ encoder =
+ MediaCodecAdapterWrapper.createForAudioEncoding(
+ new Format.Builder()
+ .setSampleMimeType(checkNotNull(inputFormat).sampleMimeType)
+ .setSampleRate(outputAudioFormat.sampleRate)
+ .setChannelCount(outputAudioFormat.channelCount)
+ .setAverageBitrate(DEFAULT_ENCODER_BITRATE)
+ .build());
+ } catch (IOException e) {
+ throw createRendererException(e);
+ }
+ encoderInputAudioFormat = outputAudioFormat;
+ return true;
+ }
+
+ /**
+ * Attempts to configure the {@link #decoder} if it has not been configured yet, and returns
+ * whether the decoder has been configured.
+ */
+ private boolean ensureDecoderConfigured() throws ExoPlaybackException {
+ if (decoder != null) {
+ return true;
+ }
+
+ FormatHolder formatHolder = getFormatHolder();
+ @SampleStream.ReadDataResult
+ int result = readSource(formatHolder, decoderInputBuffer, /* formatRequired= */ true);
+ if (result != C.RESULT_FORMAT_READ) {
+ return false;
+ }
+ inputFormat = checkNotNull(formatHolder.format);
+ try {
+ decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat);
+ } catch (IOException e) {
+ throw createRendererException(e);
+ }
+ speedProvider = new SegmentSpeedProvider(inputFormat);
+ currentSpeed = speedProvider.getSpeed(0);
+ return true;
+ }
+
+ private boolean isSpeedChanging(BufferInfo bufferInfo) {
+ if (!transformation.flattenForSlowMotion) {
+ return false;
+ }
+ float newSpeed = checkNotNull(speedProvider).getSpeed(bufferInfo.presentationTimeUs);
+ boolean speedChanging = newSpeed != currentSpeed;
+ currentSpeed = newSpeed;
+ return speedChanging;
+ }
+
+ private void flushSonicAndSetSpeed(float speed) {
+ sonicAudioProcessor.setSpeed(speed);
+ sonicAudioProcessor.setPitch(speed);
+ sonicAudioProcessor.flush();
+ }
+
+ private ExoPlaybackException createRendererException(Throwable cause) {
+ return ExoPlaybackException.createForRenderer(
+ cause, TAG, getIndex(), inputFormat, /* rendererFormatSupport= */ C.FORMAT_HANDLED);
+ }
+
+ private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) {
+ long framesWritten = bytesWritten / bytesPerFrame;
+ return framesWritten * C.MICROS_PER_SECOND / sampleRate;
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java
new file mode 100644
index 0000000000..33888226b8
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerBaseRenderer.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.MimeTypes;
+
+@RequiresApi(18)
+/* package */ abstract class TransformerBaseRenderer extends BaseRenderer {
+
+ protected final MuxerWrapper muxerWrapper;
+ protected final TransformerMediaClock mediaClock;
+ protected final Transformation transformation;
+
+ protected boolean isRendererStarted;
+
+ public TransformerBaseRenderer(
+ int trackType,
+ MuxerWrapper muxerWrapper,
+ TransformerMediaClock mediaClock,
+ Transformation transformation) {
+ super(trackType);
+ this.muxerWrapper = muxerWrapper;
+ this.mediaClock = mediaClock;
+ this.transformation = transformation;
+ }
+
+ @Override
+ @C.FormatSupport
+ public final int supportsFormat(Format format) {
+ @Nullable String sampleMimeType = format.sampleMimeType;
+ if (MimeTypes.getTrackType(sampleMimeType) != getTrackType()) {
+ return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
+ } else if (muxerWrapper.supportsSampleMimeType(sampleMimeType)) {
+ return RendererCapabilities.create(C.FORMAT_HANDLED);
+ } else {
+ return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
+ }
+ }
+
+ @Override
+ public final boolean isReady() {
+ return isSourceReady();
+ }
+
+ @Override
+ public final MediaClock getMediaClock() {
+ return mediaClock;
+ }
+
+ @Override
+ protected final void onEnabled(boolean joining, boolean mayRenderStartOfStream) {
+ muxerWrapper.registerTrack();
+ mediaClock.updateTimeForTrackType(getTrackType(), 0L);
+ }
+
+ @Override
+ protected final void onStarted() {
+ isRendererStarted = true;
+ }
+
+ @Override
+ protected final void onStopped() {
+ isRendererStarted = false;
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerMediaClock.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerMediaClock.java
new file mode 100644
index 0000000000..210eaf0ecd
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerMediaClock.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Util.minValue;
+
+import android.util.SparseLongArray;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.util.MediaClock;
+
+@RequiresApi(18)
+/* package */ final class TransformerMediaClock implements MediaClock {
+
+ private final SparseLongArray trackTypeToTimeUs;
+ private long minTrackTimeUs;
+
+ public TransformerMediaClock() {
+ trackTypeToTimeUs = new SparseLongArray();
+ }
+
+ /**
+ * Updates the time for a given track type. The clock time is computed based on the different
+ * track times.
+ */
+ public void updateTimeForTrackType(int trackType, long timeUs) {
+ long previousTimeUs = trackTypeToTimeUs.get(trackType, /* valueIfKeyNotFound= */ C.TIME_UNSET);
+ if (previousTimeUs != C.TIME_UNSET && timeUs <= previousTimeUs) {
+ // Make sure that the track times are increasing and therefore that the clock time is
+ // increasing. This is necessary for progress updates.
+ return;
+ }
+ trackTypeToTimeUs.put(trackType, timeUs);
+ if (previousTimeUs == C.TIME_UNSET || previousTimeUs == minTrackTimeUs) {
+ minTrackTimeUs = minValue(trackTypeToTimeUs);
+ }
+ }
+
+ @Override
+ public long getPositionUs() {
+ // Use minimum position among tracks as position to ensure that the buffered duration is
+ // positive. This is also useful for controlling samples interleaving.
+ return minTrackTimeUs;
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ // Playback parameters are unknown. Set default value.
+ return PlaybackParameters.DEFAULT;
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java
new file mode 100644
index 0000000000..621dab6f5c
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerVideoRenderer.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import java.nio.ByteBuffer;
+
+@RequiresApi(18)
+/* package */ final class TransformerVideoRenderer extends TransformerBaseRenderer {
+
+ private static final String TAG = "TransformerVideoRenderer";
+
+ private final DecoderInputBuffer buffer;
+
+ @Nullable private SampleTransformer sampleTransformer;
+
+ private boolean formatRead;
+ private boolean isBufferPending;
+ private boolean isInputStreamEnded;
+
+ public TransformerVideoRenderer(
+ MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) {
+ super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation);
+ buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
+ }
+
+ @Override
+ public String getName() {
+ return TAG;
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) {
+ if (!isRendererStarted || isEnded()) {
+ return;
+ }
+
+ if (!formatRead) {
+ FormatHolder formatHolder = getFormatHolder();
+ @SampleStream.ReadDataResult
+ int result = readSource(formatHolder, buffer, /* formatRequired= */ true);
+ if (result != C.RESULT_FORMAT_READ) {
+ return;
+ }
+ Format format = checkNotNull(formatHolder.format);
+ formatRead = true;
+ if (transformation.flattenForSlowMotion) {
+ sampleTransformer = new SefSlowMotionVideoSampleTransformer(format);
+ }
+ muxerWrapper.addTrackFormat(format);
+ }
+
+ while (true) {
+ // Read sample.
+ if (!isBufferPending && !readAndTransformBuffer()) {
+ return;
+ }
+ // Write sample.
+ isBufferPending =
+ !muxerWrapper.writeSample(
+ getTrackType(), buffer.data, buffer.isKeyFrame(), buffer.timeUs);
+ if (isBufferPending) {
+ return;
+ }
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return isInputStreamEnded;
+ }
+
+ /**
+ * Checks whether a sample can be read and, if so, reads it, transforms it and writes the
+ * resulting sample to the {@link #buffer}.
+ *
+ *
The buffer data can be set to null if the transformation applied discards the sample.
+ *
+ * @return Whether a sample has been read and transformed.
+ */
+ private boolean readAndTransformBuffer() {
+ buffer.clear();
+ @SampleStream.ReadDataResult
+ int result = readSource(getFormatHolder(), buffer, /* formatRequired= */ false);
+ if (result == C.RESULT_FORMAT_READ) {
+ throw new IllegalStateException("Format changes are not supported.");
+ } else if (result == C.RESULT_NOTHING_READ) {
+ return false;
+ }
+
+ // Buffer read.
+
+ if (buffer.isEndOfStream()) {
+ isInputStreamEnded = true;
+ muxerWrapper.endTrack(getTrackType());
+ return false;
+ }
+ mediaClock.updateTimeForTrackType(getTrackType(), buffer.timeUs);
+ ByteBuffer data = checkNotNull(buffer.data);
+ data.flip();
+ if (sampleTransformer != null) {
+ sampleTransformer.transformSample(buffer);
+ }
+ return true;
+ }
+}
diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/package-info.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/package-info.java
new file mode 100644
index 0000000000..1093e10882
--- /dev/null
+++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@NonNullApi
+package com.google.android.exoplayer2.transformer;
+
+import com.google.android.exoplayer2.util.NonNullApi;
diff --git a/library/transformer/src/test/AndroidManifest.xml b/library/transformer/src/test/AndroidManifest.xml
new file mode 100644
index 0000000000..0ef3273ee0
--- /dev/null
+++ b/library/transformer/src/test/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/SefSlowMotionVideoSampleTransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/SefSlowMotionVideoSampleTransformerTest.java
new file mode 100644
index 0000000000..a63db831fc
--- /dev/null
+++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/SefSlowMotionVideoSampleTransformerTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transformer;
+
+import static com.google.android.exoplayer2.transformer.SefSlowMotionVideoSampleTransformer.INPUT_FRAME_RATE;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
+import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link SefSlowMotionVideoSampleTransformer}. */
+@RunWith(AndroidJUnit4.class)
+public class SefSlowMotionVideoSampleTransformerTest {
+
+ /**
+ * Sequence of temporal SVC layers in an SEF slow motion video track with a maximum layer of 3.
+ *
+ *
Each value is attached to a frame and the sequence is repeated until there is no frame left.
+ */
+ private static final int[] LAYER_SEQUENCE_MAX_LAYER_THREE = new int[] {0, 3, 2, 3, 1, 3, 2, 3};
+
+ @Test
+ public void processCurrentFrame_240fps_keepsExpectedFrames() {
+ int captureFrameRate = 240;
+ int inputMaxLayer = 3;
+ int frameCount = 46;
+ SlowMotionData.Segment segment1 =
+ createSegment(/* startFrameIndex= */ 11, /* endFrameIndex= */ 17, /* speedDivisor= */ 2);
+ SlowMotionData.Segment segment2 =
+ createSegment(/* startFrameIndex= */ 31, /* endFrameIndex= */ 38, /* speedDivisor= */ 8);
+ Format format =
+ createSefSlowMotionFormat(
+ captureFrameRate, inputMaxLayer, Arrays.asList(segment1, segment2));
+
+ SefSlowMotionVideoSampleTransformer sampleTransformer =
+ new SefSlowMotionVideoSampleTransformer(format);
+ List outputLayers =
+ getKeptOutputLayers(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount);
+
+ List expectedLayers = Arrays.asList(0, 0, 1, 0, 0, 1, 2, 3, 0, 3, 2, 3, 1, 3, 0);
+ assertThat(outputLayers).isEqualTo(expectedLayers);
+ }
+
+ @Test
+ public void processCurrentFrame_120fps_keepsExpectedFrames() {
+ int captureFrameRate = 120;
+ int inputMaxLayer = 3;
+ int frameCount = 46;
+ SlowMotionData.Segment segment1 =
+ createSegment(/* startFrameIndex= */ 9, /* endFrameIndex= */ 17, /* speedDivisor= */ 4);
+ SlowMotionData.Segment segment2 =
+ createSegment(/* startFrameIndex= */ 31, /* endFrameIndex= */ 38, /* speedDivisor= */ 8);
+ Format format =
+ createSefSlowMotionFormat(
+ captureFrameRate, inputMaxLayer, Arrays.asList(segment1, segment2));
+
+ SefSlowMotionVideoSampleTransformer sampleTransformer =
+ new SefSlowMotionVideoSampleTransformer(format);
+ List outputLayers =
+ getKeptOutputLayers(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount);
+
+ List expectedLayers =
+ Arrays.asList(0, 1, 0, 3, 2, 3, 1, 3, 2, 3, 0, 1, 0, 1, 2, 3, 0, 3, 2, 3, 1, 3, 0, 1);
+ assertThat(outputLayers).isEqualTo(expectedLayers);
+ }
+
+ @Test
+ public void processCurrentFrame_contiguousSegments_keepsExpectedFrames() {
+ int captureFrameRate = 240;
+ int inputMaxLayer = 3;
+ int frameCount = 26;
+ SlowMotionData.Segment segment1 =
+ createSegment(/* startFrameIndex= */ 11, /* endFrameIndex= */ 19, /* speedDivisor= */ 2);
+ SlowMotionData.Segment segment2 =
+ createSegment(/* startFrameIndex= */ 19, /* endFrameIndex= */ 22, /* speedDivisor= */ 8);
+ Format format =
+ createSefSlowMotionFormat(
+ captureFrameRate, inputMaxLayer, Arrays.asList(segment1, segment2));
+
+ SefSlowMotionVideoSampleTransformer sampleTransformer =
+ new SefSlowMotionVideoSampleTransformer(format);
+ List outputLayers =
+ getKeptOutputLayers(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount);
+
+ List expectedLayers = Arrays.asList(0, 0, 1, 0, 2, 3, 1, 3, 0);
+ assertThat(outputLayers).isEqualTo(expectedLayers);
+ }
+
+ @Test
+ public void processCurrentFrame_skipsSegmentsWithNoFrame() {
+ int captureFrameRate = 240;
+ int inputMaxLayer = 3;
+ int frameCount = 16;
+ SlowMotionData.Segment segmentWithNoFrame1 =
+ new SlowMotionData.Segment(
+ /* startTimeMs= */ 120, /* endTimeMs= */ 130, /* speedDivisor= */ 2);
+ SlowMotionData.Segment segmentWithNoFrame2 =
+ new SlowMotionData.Segment(
+ /* startTimeMs= */ 270, /* endTimeMs= */ 280, /* speedDivisor= */ 2);
+ SlowMotionData.Segment segmentWithFrame =
+ createSegment(/* startFrameIndex= */ 11, /* endFrameIndex= */ 16, /* speedDivisor= */ 2);
+ Format format =
+ createSefSlowMotionFormat(
+ captureFrameRate,
+ inputMaxLayer,
+ Arrays.asList(segmentWithNoFrame1, segmentWithNoFrame2, segmentWithFrame));
+
+ SefSlowMotionVideoSampleTransformer sampleTransformer =
+ new SefSlowMotionVideoSampleTransformer(format);
+ List outputLayers =
+ getKeptOutputLayers(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount);
+
+ List expectedLayers = Arrays.asList(0, 0, 1);
+ assertThat(outputLayers).isEqualTo(expectedLayers);
+ }
+
+ @Test
+ public void getCurrentFrameOutputTimeUs_240fps_outputsExpectedTimes() {
+ int captureFrameRate = 240;
+ int inputMaxLayer = 3;
+ int frameCount = 16;
+ SlowMotionData.Segment segment1 =
+ new SlowMotionData.Segment(
+ /* startTimeMs= */ 50, /* endTimeMs= */ 150, /* speedDivisor= */ 2);
+ SlowMotionData.Segment segment2 =
+ new SlowMotionData.Segment(
+ /* startTimeMs= */ 210, /* endTimeMs= */ 360, /* speedDivisor= */ 8);
+ Format format =
+ createSefSlowMotionFormat(
+ captureFrameRate, inputMaxLayer, Arrays.asList(segment1, segment2));
+
+ SefSlowMotionVideoSampleTransformer sampleTransformer =
+ new SefSlowMotionVideoSampleTransformer(format);
+ List