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: + * + *

+ * + * @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: + * + *

* * @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 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 outputTimesUs = + getOutputTimesUs(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount); + + // Test frame inside segment. + assertThat(outputTimesUs.get(9)) + .isEqualTo(Math.round((300.0 + 100 + (300 - 210) * 7) * 1000 * 30 / 240)); + // Test frame outside segment. + assertThat(outputTimesUs.get(13)) + .isEqualTo(Math.round((433 + 1 / 3.0 + 100 + 150 * 7) * 1000 * 30 / 240)); + } + + @Test + public void getCurrentFrameOutputTimeUs_120fps_outputsExpectedTimes() { + int captureFrameRate = 120; + 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 outputTimesUs = + getOutputTimesUs(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount); + + // Test frame inside segment. + assertThat(outputTimesUs.get(9)) + .isEqualTo(Math.round((300.0 + 100 + (300 - 210) * 7) * 1000 * 30 / 120)); + // Test frame outside segment. + assertThat(outputTimesUs.get(13)) + .isEqualTo(Math.round((433 + 1 / 3.0 + 100 + 150 * 7) * 1000 * 30 / 120)); + } + + @Test + public void getCurrentFrameOutputTimeUs_contiguousSegments_outputsExpectedTimes() { + int captureFrameRate = 240; + int inputMaxLayer = 3; + int frameCount = 16; + SlowMotionData.Segment segment1 = + new SlowMotionData.Segment( + /* startTimeMs= */ 50, /* endTimeMs= */ 210, /* 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 outputTimesUs = + getOutputTimesUs(sampleTransformer, LAYER_SEQUENCE_MAX_LAYER_THREE, frameCount); + + // Test frame inside second segment. + assertThat(outputTimesUs.get(9)).isEqualTo(136_250); + } + + /** + * Creates a {@link SlowMotionData.Segment}. + * + * @param startFrameIndex The index of the first frame in the segment. + * @param endFrameIndex The index of the first frame following the segment. + * @param speedDivisor The factor by which the input is slowed down in the segment. + * @return A {@link SlowMotionData.Segment}. + */ + private static SlowMotionData.Segment createSegment( + int startFrameIndex, int endFrameIndex, int speedDivisor) { + return new SlowMotionData.Segment( + /* startTimeMs= */ (int) (startFrameIndex * C.MILLIS_PER_SECOND / INPUT_FRAME_RATE), + /* endTimeMs= */ (int) (endFrameIndex * C.MILLIS_PER_SECOND / INPUT_FRAME_RATE) - 1, + speedDivisor); + } + + /** Creates a {@link Format} for an SEF slow motion video track. */ + private static Format createSefSlowMotionFormat( + int captureFrameRate, + int inputMaxLayer, + List segments) { + SmtaMetadataEntry smtaMetadataEntry = + new SmtaMetadataEntry(captureFrameRate, /* svcTemporalLayerCount= */ inputMaxLayer + 1); + SlowMotionData slowMotionData = new SlowMotionData(segments); + Metadata metadata = new Metadata(smtaMetadataEntry, slowMotionData); + return new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setMetadata(metadata) + .build(); + } + + /** + * Returns a list containing the temporal SVC layers of the frames that should be kept according + * to {@link SefSlowMotionVideoSampleTransformer#processCurrentFrame(int, long)}. + * + * @param sampleTransformer The {@link SefSlowMotionVideoSampleTransformer}. + * @param layerSequence The sequence of layer values in the input. + * @param frameCount The number of video frames in the input. + * @return The output layers. + */ + private static List getKeptOutputLayers( + SefSlowMotionVideoSampleTransformer sampleTransformer, + int[] layerSequence, + int frameCount) { + List outputLayers = new ArrayList<>(); + for (int i = 0; i < frameCount; i++) { + int layer = layerSequence[i % layerSequence.length]; + long timeUs = i * C.MICROS_PER_SECOND / INPUT_FRAME_RATE; + if (sampleTransformer.processCurrentFrame(layer, timeUs)) { + outputLayers.add(layer); + } + } + return outputLayers; + } + + /** + * Returns a list containing the frame output times obtained using {@link + * SefSlowMotionVideoSampleTransformer#getCurrentFrameOutputTimeUs(long)}. + * + *

The output contains the output times for all the input frames, regardless of whether they + * should be kept or not. + * + * @param sampleTransformer The {@link SefSlowMotionVideoSampleTransformer}. + * @param layerSequence The sequence of layer values in the input. + * @param frameCount The number of video frames in the input. + * @return The frame output times, in microseconds. + */ + private static List getOutputTimesUs( + SefSlowMotionVideoSampleTransformer sampleTransformer, + int[] layerSequence, + int frameCount) { + List outputTimesUs = new ArrayList<>(); + for (int i = 0; i < frameCount; i++) { + int layer = layerSequence[i % layerSequence.length]; + long inputTimeUs = i * C.MICROS_PER_SECOND / INPUT_FRAME_RATE; + sampleTransformer.processCurrentFrame(layer, inputTimeUs); + outputTimesUs.add(sampleTransformer.getCurrentFrameOutputTimeUs(inputTimeUs)); + } + return outputTimesUs; + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/SegmentSpeedProviderTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/SegmentSpeedProviderTest.java new file mode 100644 index 0000000000..616443e963 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/SegmentSpeedProviderTest.java @@ -0,0 +1,85 @@ +/* + * 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.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +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.SlowMotionData.Segment; +import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link SegmentSpeedProvider}. */ +@RunWith(AndroidJUnit4.class) +public class SegmentSpeedProviderTest { + + private static final SmtaMetadataEntry SMTA_SPEED_8 = + new SmtaMetadataEntry(/* captureFrameRate= */ 240, /* svcTemporalLayerCount= */ 4); + + @Test + public void getSpeed_noSegments_returnsBaseSpeed() { + SegmentSpeedProvider provider = + new SegmentSpeedProvider( + new Format.Builder().setMetadata(new Metadata(SMTA_SPEED_8)).build()); + assertThat(provider.getSpeed(0)).isEqualTo(8); + assertThat(provider.getSpeed(1_000_000)).isEqualTo(8); + } + + @Test + public void getSpeed_returnsCorrectSpeed() { + List segments = + ImmutableList.of( + new Segment(/* startTimeMs= */ 500, /* endTimeMs= */ 1000, /* speedDivisor= */ 8), + new Segment(/* startTimeMs= */ 1500, /* endTimeMs= */ 2000, /* speedDivisor= */ 4), + new Segment(/* startTimeMs= */ 2000, /* endTimeMs= */ 2500, /* speedDivisor= */ 2)); + + SegmentSpeedProvider provider = + new SegmentSpeedProvider( + new Format.Builder() + .setMetadata(new Metadata(new SlowMotionData(segments), SMTA_SPEED_8)) + .build()); + + assertThat(provider.getSpeed(C.msToUs(0))).isEqualTo(8); + assertThat(provider.getSpeed(C.msToUs(500))).isEqualTo(1); + assertThat(provider.getSpeed(C.msToUs(800))).isEqualTo(1); + assertThat(provider.getSpeed(C.msToUs(1000))).isEqualTo(8); + assertThat(provider.getSpeed(C.msToUs(1250))).isEqualTo(8); + assertThat(provider.getSpeed(C.msToUs(1500))).isEqualTo(2); + assertThat(provider.getSpeed(C.msToUs(1650))).isEqualTo(2); + assertThat(provider.getSpeed(C.msToUs(2000))).isEqualTo(4); + assertThat(provider.getSpeed(C.msToUs(2400))).isEqualTo(4); + assertThat(provider.getSpeed(C.msToUs(2500))).isEqualTo(8); + assertThat(provider.getSpeed(C.msToUs(3000))).isEqualTo(8); + } + + @Test + public void getSpeed_withNegativeTimestamp_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SegmentSpeedProvider( + new Format.Builder().setMetadata(new Metadata(SMTA_SPEED_8)).build()) + .getSpeed(-1)); + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java new file mode 100644 index 0000000000..e4835274c2 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TestMuxer.java @@ -0,0 +1,109 @@ +/* + * 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 com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.testutil.DumpableFormat; +import com.google.android.exoplayer2.testutil.Dumper; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * An implementation of {@link Muxer} that supports dumping information about all interactions (for + * testing purposes) and delegates the actual muxing operations to a {@link FrameworkMuxer}. + */ +public final class TestMuxer implements Muxer, Dumper.Dumpable { + + private final Muxer frameworkMuxer; + private final List dumpables; + + /** Creates a new test muxer. */ + public TestMuxer(String path, String outputMimeType) throws IOException { + frameworkMuxer = new FrameworkMuxer.Factory().create(path, outputMimeType); + dumpables = new ArrayList<>(); + dumpables.add(dumper -> dumper.add("containerMimeType", outputMimeType)); + } + + // Muxer implementation. + + @Override + public boolean supportsSampleMimeType(String mimeType) { + return frameworkMuxer.supportsSampleMimeType(mimeType); + } + + @Override + public int addTrack(Format format) { + int trackIndex = frameworkMuxer.addTrack(format); + dumpables.add(new DumpableFormat(format, trackIndex)); + return trackIndex; + } + + @Override + public void writeSampleData( + int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) { + dumpables.add(new DumpableSample(trackIndex, data, isKeyFrame, presentationTimeUs)); + frameworkMuxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); + } + + @Override + public void release(boolean forCancellation) { + dumpables.add(dumper -> dumper.add("released", true)); + frameworkMuxer.release(forCancellation); + } + + // Dumper.Dumpable implementation. + + @Override + public void dump(Dumper dumper) { + for (Dumper.Dumpable dumpable : dumpables) { + dumpable.dump(dumper); + } + } + + private static final class DumpableSample implements Dumper.Dumpable { + + private final int trackIndex; + private final long presentationTimeUs; + private final boolean isKeyFrame; + private final int sampleDataHashCode; + + public DumpableSample( + int trackIndex, ByteBuffer sample, boolean isKeyFrame, long presentationTimeUs) { + this.trackIndex = trackIndex; + this.presentationTimeUs = presentationTimeUs; + this.isKeyFrame = isKeyFrame; + int initialPosition = sample.position(); + byte[] data = new byte[sample.remaining()]; + sample.get(data); + sample.position(initialPosition); + sampleDataHashCode = Arrays.hashCode(data); + } + + @Override + public void dump(Dumper dumper) { + dumper + .startBlock("sample") + .add("trackIndex", trackIndex) + .add("dataHashCode", sampleDataHashCode) + .add("isKeyFrame", isKeyFrame) + .add("presentationTimeUs", presentationTimeUs) + .endBlock(); + } + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java new file mode 100644 index 0000000000..8cfba3156d --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerBuilderTest.java @@ -0,0 +1,57 @@ +/* + * 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 org.junit.Assert.assertThrows; + +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link Transformer.Builder}. */ +@RunWith(AndroidJUnit4.class) +public class TransformerBuilderTest { + + @Test + public void setOutputMimeType_unsupportedMimeType_throws() { + assertThrows( + IllegalStateException.class, + () -> new Transformer.Builder().setOutputMimeType(MimeTypes.VIDEO_FLV).build()); + } + + @Test + public void build_withoutContext_throws() { + assertThrows(IllegalStateException.class, () -> new Transformer.Builder().build()); + } + + @Test + public void build_removeAudioAndVideo_throws() { + Context context = ApplicationProvider.getApplicationContext(); + + assertThrows( + IllegalStateException.class, + () -> + new Transformer.Builder() + .setContext(context) + .setRemoveAudio(true) + .setRemoveVideo(true) + .build()); + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java new file mode 100644 index 0000000000..9dc96421c0 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -0,0 +1,583 @@ +/* + * 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.Transformer.PROGRESS_STATE_AVAILABLE; +import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION; +import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE; +import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; +import com.google.android.exoplayer2.testutil.DumpFileAsserts; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.Iterables; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowMediaCodec; + +/** Unit test for {@link Transformer}. */ +@RunWith(AndroidJUnit4.class) +public final class TransformerTest { + + private static final String URI_PREFIX = "asset:///media/"; + private static final String FILE_VIDEO_ONLY = "mkv/sample.mkv"; + private static final String FILE_AUDIO_ONLY = "amr/sample_nb.amr"; + private static final String FILE_AUDIO_VIDEO = "mp4/sample.mp4"; + private static final String FILE_WITH_SUBTITLES = "mkv/sample_with_srt.mkv"; + private static final String FILE_WITH_SEF_SLOW_MOTION = "mp4/sample_sef_slow_motion.mp4"; + private static final String FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED = "mp4/sample_ac3.mp4"; + private static final String FILE_UNKNOWN_DURATION = "mp4/sample_fragmented.mp4"; + public static final String DUMP_FILE_OUTPUT_DIRECTORY = "transformerdumps"; + public static final String DUMP_FILE_EXTENSION = "dump"; + + private Context context; + private String outputPath; + private TestMuxer testMuxer; + private AutoAdvancingFakeClock clock; + private ProgressHolder progressHolder; + + @Before + public void setUp() throws Exception { + context = ApplicationProvider.getApplicationContext(); + outputPath = Util.createTempFile(context, "TransformerTest").getPath(); + clock = new AutoAdvancingFakeClock(); + progressHolder = new ProgressHolder(); + createEncodersAndDecoders(); + } + + @After + public void tearDown() throws Exception { + Files.delete(Paths.get(outputPath)); + removeEncodersAndDecoders(); + } + + @Test + public void startTransformation_videoOnly_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_VIDEO_ONLY)); + } + + @Test + public void startTransformation_audioOnly_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_ONLY)); + } + + @Test + public void startTransformation_audioAndVideo_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); + } + + @Test + public void startTransformation_withSubtitles_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_SUBTITLES); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SUBTITLES)); + } + + @Test + public void startTransformation_successiveTransformations_completesSuccessfully() + throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + // Transform first media item. + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + Files.delete(Paths.get(outputPath)); + + // Transform second media item. + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_VIDEO_ONLY)); + } + + @Test + public void startTransformation_concurrentTransformations_throwsError() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + + assertThrows( + IllegalStateException.class, () -> transformer.startTransformation(mediaItem, outputPath)); + } + + @Test + public void startTransformation_removeAudio_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setRemoveAudio(true) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput( + context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".noaudio")); + } + + @Test + public void startTransformation_removeVideo_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setRemoveVideo(true) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput( + context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".novideo")); + } + + @Test + public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setFlattenForSlowMotion(true) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_SEF_SLOW_MOTION); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SEF_SLOW_MOTION)); + } + + @Test + public void startTransformation_withPlayerError_completesWithError() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4"); + + transformer.startTransformation(mediaItem, outputPath); + Exception exception = TransformerTestRunner.runUntilError(transformer); + + assertThat(exception).isInstanceOf(ExoPlaybackException.class); + assertThat(exception).hasCauseThat().isInstanceOf(IOException.class); + } + + @Test + public void startTransformation_withAllSampleFormatsUnsupported_completesWithError() + throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED); + + transformer.startTransformation(mediaItem, outputPath); + Exception exception = TransformerTestRunner.runUntilError(transformer); + + assertThat(exception).isInstanceOf(IllegalStateException.class); + } + + @Test + public void startTransformation_afterCancellation_completesSuccessfully() throws Exception { + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + transformer.cancel(); + Files.delete(Paths.get(outputPath)); + + // This would throw if the previous transformation had not been cancelled. + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_VIDEO_ONLY)); + } + + @Test + public void startTransformation_fromSpecifiedThread_completesSuccessfully() throws Exception { + HandlerThread anotherThread = new HandlerThread("AnotherThread"); + anotherThread.start(); + Looper looper = anotherThread.getLooper(); + Transformer transformer = + new Transformer.Builder() + .setContext(context) + .setLooper(looper) + .setClock(clock) + .setMuxerFactory(new TestMuxerFactory()) + .build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + AtomicReference exception = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + new Handler(looper) + .post( + () -> { + try { + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + } catch (Exception e) { + exception.set(e); + } finally { + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + + assertThat(exception.get()).isNull(); + DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_ONLY)); + } + + @Test + public void startTransformation_fromWrongThread_throwsError() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY); + HandlerThread anotherThread = new HandlerThread("AnotherThread"); + AtomicReference illegalStateException = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + anotherThread.start(); + new Handler(anotherThread.getLooper()) + .post( + () -> { + try { + transformer.startTransformation(mediaItem, outputPath); + } catch (IOException e) { + // Do nothing. + } catch (IllegalStateException e) { + illegalStateException.set(e); + } finally { + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + + assertThat(illegalStateException.get()).isNotNull(); + } + + @Test + public void getProgress_knownDuration_returnsConsistentStates() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + AtomicInteger previousProgressState = + new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); + AtomicBoolean foundInconsistentState = new AtomicBoolean(); + Handler progressHandler = + new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + @Transformer.ProgressState int progressState = transformer.getProgress(progressHolder); + if (progressState == PROGRESS_STATE_UNAVAILABLE) { + foundInconsistentState.set(true); + return; + } + switch (previousProgressState.get()) { + case PROGRESS_STATE_WAITING_FOR_AVAILABILITY: + break; + case PROGRESS_STATE_AVAILABLE: + if (progressState == PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { + foundInconsistentState.set(true); + return; + } + break; + case PROGRESS_STATE_NO_TRANSFORMATION: + if (progressState != PROGRESS_STATE_NO_TRANSFORMATION) { + foundInconsistentState.set(true); + return; + } + break; + default: + throw new IllegalStateException(); + } + previousProgressState.set(progressState); + sendEmptyMessage(0); + } + }; + + transformer.startTransformation(mediaItem, outputPath); + progressHandler.sendEmptyMessage(0); + TransformerTestRunner.runUntilCompleted(transformer); + + assertThat(foundInconsistentState.get()).isFalse(); + } + + @Test + public void getProgress_knownDuration_givesIncreasingPercentages() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + List progresses = new ArrayList<>(); + Handler progressHandler = + new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + @Transformer.ProgressState int progressState = transformer.getProgress(progressHolder); + if (progressState == PROGRESS_STATE_NO_TRANSFORMATION) { + return; + } + if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY + && (progresses.isEmpty() + || Iterables.getLast(progresses) != progressHolder.progress)) { + progresses.add(progressHolder.progress); + } + sendEmptyMessage(0); + } + }; + + transformer.startTransformation(mediaItem, outputPath); + progressHandler.sendEmptyMessage(0); + TransformerTestRunner.runUntilCompleted(transformer); + + assertThat(progresses).isInOrder(); + if (!progresses.isEmpty()) { + // The progress list could be empty if the transformation ends before any progress can be + // retrieved. + assertThat(progresses.get(0)).isAtLeast(0); + assertThat(Iterables.getLast(progresses)).isLessThan(100); + } + } + + @Test + public void getProgress_noCurrentTransformation_returnsNoTransformation() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + @Transformer.ProgressState int stateBeforeTransform = transformer.getProgress(progressHolder); + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + @Transformer.ProgressState int stateAfterTransform = transformer.getProgress(progressHolder); + + assertThat(stateBeforeTransform).isEqualTo(Transformer.PROGRESS_STATE_NO_TRANSFORMATION); + assertThat(stateAfterTransform).isEqualTo(Transformer.PROGRESS_STATE_NO_TRANSFORMATION); + } + + @Test + public void getProgress_unknownDuration_returnsConsistentStates() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_UNKNOWN_DURATION); + AtomicInteger previousProgressState = + new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); + AtomicBoolean foundInconsistentState = new AtomicBoolean(); + Handler progressHandler = + new Handler(Looper.myLooper()) { + @Override + public void handleMessage(Message msg) { + @Transformer.ProgressState int progressState = transformer.getProgress(progressHolder); + switch (previousProgressState.get()) { + case PROGRESS_STATE_WAITING_FOR_AVAILABILITY: + break; + case PROGRESS_STATE_UNAVAILABLE: + case PROGRESS_STATE_AVAILABLE: // See [Internal: b/176145097]. + if (progressState == PROGRESS_STATE_WAITING_FOR_AVAILABILITY) { + foundInconsistentState.set(true); + return; + } + break; + case PROGRESS_STATE_NO_TRANSFORMATION: + if (progressState != PROGRESS_STATE_NO_TRANSFORMATION) { + foundInconsistentState.set(true); + return; + } + break; + default: + throw new IllegalStateException(); + } + previousProgressState.set(progressState); + sendEmptyMessage(0); + } + }; + + transformer.startTransformation(mediaItem, outputPath); + progressHandler.sendEmptyMessage(0); + TransformerTestRunner.runUntilCompleted(transformer); + + assertThat(foundInconsistentState.get()).isFalse(); + } + + @Test + public void getProgress_fromWrongThread_throwsError() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + HandlerThread anotherThread = new HandlerThread("AnotherThread"); + AtomicReference illegalStateException = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + anotherThread.start(); + new Handler(anotherThread.getLooper()) + .post( + () -> { + try { + transformer.getProgress(progressHolder); + } catch (IllegalStateException e) { + illegalStateException.set(e); + } finally { + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + + assertThat(illegalStateException.get()).isNotNull(); + } + + @Test + public void cancel_afterCompletion_doesNotThrow() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); + + transformer.startTransformation(mediaItem, outputPath); + TransformerTestRunner.runUntilCompleted(transformer); + transformer.cancel(); + } + + @Test + public void cancel_fromWrongThread_throwsError() throws Exception { + Transformer transformer = new Transformer.Builder().setContext(context).setClock(clock).build(); + HandlerThread anotherThread = new HandlerThread("AnotherThread"); + AtomicReference illegalStateException = new AtomicReference<>(); + CountDownLatch countDownLatch = new CountDownLatch(1); + + anotherThread.start(); + new Handler(anotherThread.getLooper()) + .post( + () -> { + try { + transformer.cancel(); + } catch (IllegalStateException e) { + illegalStateException.set(e); + } finally { + countDownLatch.countDown(); + } + }); + countDownLatch.await(); + + assertThat(illegalStateException.get()).isNotNull(); + } + + private static void createEncodersAndDecoders() { + ShadowMediaCodec.CodecConfig codecConfig = + new ShadowMediaCodec.CodecConfig( + /* inputBufferSize= */ 10_000, + /* outputBufferSize= */ 10_000, + /* codec= */ (in, out) -> out.put(in)); + ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AAC, codecConfig); + ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig); + ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, codecConfig); + } + + private static void removeEncodersAndDecoders() { + ShadowMediaCodec.clearCodecs(); + } + + private static String getDumpFileName(String originalFileName) { + return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION; + } + + private final class TestMuxerFactory implements Muxer.Factory { + @Override + public Muxer create(String path, String outputMimeType) throws IOException { + testMuxer = new TestMuxer(path, outputMimeType); + return testMuxer; + } + + @Override + public Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType) + throws IOException { + testMuxer = new TestMuxer("FD:" + parcelFileDescriptor.getFd(), outputMimeType); + return testMuxer; + } + + @Override + public boolean supportsOutputMimeType(String mimeType) { + return true; + } + } +} diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java new file mode 100644 index 0000000000..1eacbc46e0 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java @@ -0,0 +1,93 @@ +/* + * 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.robolectric.RobolectricUtil.runLooperUntil; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.robolectric.RobolectricUtil; +import java.util.concurrent.TimeoutException; + +/** Helper class to run a {@link Transformer} test. */ +public final class TransformerTestRunner { + + private TransformerTestRunner() {} + + /** + * Runs tasks of the {@link Transformer#getApplicationLooper() transformer Looper} until the + * current {@link Transformer transformation} completes. + * + * @param transformer The {@link Transformer}. + * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + * @throws IllegalStateException If the method is not called from the main thread, or if the + * transformation completes with error. + */ + public static void runUntilCompleted(Transformer transformer) throws TimeoutException { + @Nullable Exception exception = runUntilListenerCalled(transformer); + if (exception != null) { + throw new IllegalStateException(exception); + } + } + + /** + * Runs tasks of the {@link Transformer#getApplicationLooper() transformer Looper} until a {@link + * Transformer} error occurs. + * + * @param transformer The {@link Transformer}. + * @return The raised exception. + * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + * @throws IllegalStateException If the method is not called from the main thread, or if the + * transformation completes without error. + */ + public static Exception runUntilError(Transformer transformer) throws TimeoutException { + @Nullable Exception exception = runUntilListenerCalled(transformer); + if (exception == null) { + throw new IllegalStateException("The transformation completed without error."); + } + return exception; + } + + @Nullable + private static Exception runUntilListenerCalled(Transformer transformer) throws TimeoutException { + TransformationResult transformationResult = new TransformationResult(); + Transformer.Listener listener = + new Transformer.Listener() { + @Override + public void onTransformationCompleted(MediaItem inputMediaItem) { + transformationResult.isCompleted = true; + } + + @Override + public void onTransformationError(MediaItem inputMediaItem, Exception exception) { + transformationResult.exception = exception; + } + }; + transformer.setListener(listener); + runLooperUntil( + transformer.getApplicationLooper(), + () -> transformationResult.isCompleted || transformationResult.exception != null); + return transformationResult.exception; + } + + private static class TransformationResult { + public boolean isCompleted; + @Nullable public Exception exception; + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 44711a76a5..23e43fc5f3 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -51,6 +51,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackPreparer; @@ -66,6 +67,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; 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.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; @@ -772,8 +774,11 @@ public class StyledPlayerControlView extends FrameLayout { if (player != null) { player.addListener(componentListener); } - if (player != null && player.getTrackSelector() instanceof DefaultTrackSelector) { - this.trackSelector = (DefaultTrackSelector) player.getTrackSelector(); + if (player instanceof ExoPlayer) { + TrackSelector trackSelector = ((ExoPlayer) player).getTrackSelector(); + if (trackSelector instanceof DefaultTrackSelector) { + this.trackSelector = (DefaultTrackSelector) trackSelector; + } } else { this.trackSelector = null; } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index af719a117d..da072cf193 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -46,9 +46,9 @@ import com.google.android.exoplayer2.testutil.ExoHostedTest; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; @@ -260,7 +260,7 @@ import java.util.List; @Override protected DrmSessionManager buildDrmSessionManager() { if (widevineLicenseUrl == null) { - return DrmSessionManager.getDummyDrmSessionManager(); + return DrmSessionManager.DRM_UNSUPPORTED; } MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory()); @@ -386,7 +386,7 @@ import java.util.List; } @Override - protected TrackSelection.Definition[] selectAllTracks( + protected ExoTrackSelection.Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, @@ -399,10 +399,10 @@ import java.util.List; TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX); Assertions.checkState(videoTrackGroups.length == 1); Assertions.checkState(audioTrackGroups.length == 1); - TrackSelection.Definition[] definitions = - new TrackSelection.Definition[mappedTrackInfo.getRendererCount()]; + ExoTrackSelection.Definition[] definitions = + new ExoTrackSelection.Definition[mappedTrackInfo.getRendererCount()]; definitions[VIDEO_RENDERER_INDEX] = - new TrackSelection.Definition( + new ExoTrackSelection.Definition( videoTrackGroups.get(0), getVideoTrackIndices( videoTrackGroups.get(0), @@ -410,7 +410,7 @@ import java.util.List; videoFormatIds, canIncludeAdditionalVideoFormats)); definitions[AUDIO_RENDERER_INDEX] = - new TrackSelection.Definition( + new ExoTrackSelection.Definition( audioTrackGroups.get(0), getTrackIndex(audioTrackGroups.get(0), audioFormatId)); includedAdditionalVideoFormats = definitions[VIDEO_RENDERER_INDEX].tracks.length > videoFormatIds.length; diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/RandomizedMp3Decoder.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/RandomizedMp3Decoder.java index 1b033e1955..3edc847273 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/RandomizedMp3Decoder.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/RandomizedMp3Decoder.java @@ -20,6 +20,7 @@ import android.media.AudioFormat; import android.media.MediaCrypto; import android.media.MediaFormat; import android.view.Surface; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.audio.MpegAudioUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Assertions; @@ -41,6 +42,7 @@ import org.robolectric.shadows.ShadowMediaCodec; * *

All the data written to the output by the decoder can be obtained by getAllOutputBytes(). */ +@RequiresApi(29) public final class RandomizedMp3Decoder implements ShadowMediaCodec.CodecConfig.Codec { private final List decoderOutput = new ArrayList<>(); private int frameSizeInBytes; @@ -70,9 +72,6 @@ public final class RandomizedMp3Decoder implements ShadowMediaCodec.CodecConfig. @Override public void onConfigured(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) { - // Both getInteger and getString require API29. This class is only used in EndToEndGaplessTest - // that only runs on - // API29. int pcmEncoding = format.getInteger( MediaFormat.KEY_PCM_ENCODING, /* defaultValue= */ AudioFormat.ENCODING_PCM_16BIT); diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java index 31a026014e..68a9362a13 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java @@ -78,6 +78,11 @@ public final class ShadowMediaCodecConfig extends ExternalResource { configureCodec("exotest.audio.mpegl2", MimeTypes.AUDIO_MPEG_L2); configureCodec("exotest.audio.opus", MimeTypes.AUDIO_OPUS); configureCodec("exotest.audio.vorbis", MimeTypes.AUDIO_VORBIS); + + // Raw audio should use a bypass mode and never need this codec. However, to easily assert + // failures of the bypass mode we want to detect when the raw audio is decoded by this class and + // thus we need a codec to output samples. + configureCodec("exotest.audio.raw", MimeTypes.AUDIO_RAW); } @Override diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java index 55a55ab059..b813a93d2a 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.robolectric; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; -import android.os.Handler; import android.os.Looper; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -297,21 +296,24 @@ public class TestPlayerRunHelper { public static void playUntilPosition(ExoPlayer player, int windowIndex, long positionMs) throws TimeoutException { verifyMainTestThread(player); - Handler testHandler = Util.createHandlerForCurrentOrMainLooper(); - + Looper applicationLooper = Util.getCurrentOrMainLooper(); AtomicBoolean messageHandled = new AtomicBoolean(false); player .createMessage( (messageType, payload) -> { // Block playback thread until pause command has been sent from test thread. ConditionVariable blockPlaybackThreadCondition = new ConditionVariable(); - testHandler.post( - () -> { - player.pause(); - messageHandled.set(true); - blockPlaybackThreadCondition.open(); - }); + player + .getClock() + .createHandler(applicationLooper, /* callback= */ null) + .post( + () -> { + player.pause(); + messageHandled.set(true); + blockPlaybackThreadCondition.open(); + }); try { + player.getClock().onThreadBlocked(); blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { // Ignore. @@ -354,7 +356,7 @@ public class TestPlayerRunHelper { AtomicBoolean receivedMessageCallback = new AtomicBoolean(false); player .createMessage((type, data) -> receivedMessageCallback.set(true)) - .setHandler(Util.createHandlerForCurrentOrMainLooper()) + .setLooper(Util.getCurrentOrMainLooper()) .send(); runMainLooperUntil(receivedMessageCallback::get); } diff --git a/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.0.dump b/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.0.dump index db94ad32bc..bb201ef2ef 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.0.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.0.dump @@ -3,7 +3,7 @@ seekMap: duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.unknown_length.dump b/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.unknown_length.dump index db94ad32bc..bb201ef2ef 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.unknown_length.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/non-motion-photo-shortened.jpg.unknown_length.dump @@ -3,7 +3,7 @@ seekMap: duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.0.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.0.dump index cb4bccefa5..7e398127fa 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.0.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.0.dump @@ -1,9 +1,10 @@ seekMap: - isSeekable = false + isSeekable = true duration = UNSET TIME - getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=131582]] + getPosition(1) = [[timeUs=0, position=131582]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.unknown_length.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.unknown_length.dump index db94ad32bc..bb201ef2ef 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.unknown_length.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-shortened.jpg.unknown_length.dump @@ -3,7 +3,7 @@ seekMap: duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.0.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.0.dump index db94ad32bc..bb201ef2ef 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.0.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.0.dump @@ -3,7 +3,7 @@ seekMap: duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.unknown_length.dump b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.unknown_length.dump index db94ad32bc..bb201ef2ef 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.unknown_length.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/pixel-motion-photo-video-removed-shortened.jpg.unknown_length.dump @@ -3,7 +3,7 @@ seekMap: duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.0.dump b/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.0.dump index b9b6c3b614..df80dc226a 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.0.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.0.dump @@ -1,9 +1,10 @@ seekMap: - isSeekable = false + isSeekable = true duration = UNSET TIME - getPosition(0) = [[timeUs=0, position=0]] + getPosition(0) = [[timeUs=0, position=20345]] + getPosition(1) = [[timeUs=0, position=20345]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.unknown_length.dump b/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.unknown_length.dump index db94ad32bc..bb201ef2ef 100644 --- a/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.unknown_length.dump +++ b/testdata/src/test/assets/extractordumps/jpeg/ss-motion-photo-shortened.jpg.unknown_length.dump @@ -3,7 +3,7 @@ seekMap: duration = UNSET TIME getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 -track 0: +track 1024: total output bytes = 0 sample count = 0 format 0: diff --git a/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.0.dump b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.0.dump new file mode 100644 index 0000000000..b3cfcbe766 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.0.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1072000 + getPosition(0) = [[timeUs=0, position=4312]] + getPosition(1) = [[timeUs=0, position=4312]] + getPosition(536000) = [[timeUs=0, position=4312]] + getPosition(1072000) = [[timeUs=0, position=4312]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 7282 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/vorbis + maxInputSize = 8192 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash 71A77B76 + data = length 3189, hash 90EA712C + sample 0: + time = 59000 + flags = 1 + data = length 5, hash 1B4605F + sample 1: + time = 62000 + flags = 1 + data = length 24, hash 12FEB31D + sample 2: + time = 75000 + flags = 1 + data = length 139, hash 837FE175 + sample 3: + time = 98000 + flags = 1 + data = length 165, hash 9C52658 + sample 4: + time = 121000 + flags = 1 + data = length 166, hash 29F7D096 + sample 5: + time = 145000 + flags = 1 + data = length 162, hash 91BB916A + sample 6: + time = 168000 + flags = 1 + data = length 166, hash 42E61A0D + sample 7: + time = 191000 + flags = 1 + data = length 173, hash 69DCAA15 + sample 8: + time = 214000 + flags = 1 + data = length 171, hash 6BEB915E + sample 9: + time = 237000 + flags = 1 + data = length 162, hash 8580596B + sample 10: + time = 261000 + flags = 1 + data = length 174, hash 4561AB7E + sample 11: + time = 284000 + flags = 1 + data = length 169, hash E9CCB702 + sample 12: + time = 307000 + flags = 1 + data = length 168, hash 2C07206 + sample 13: + time = 330000 + flags = 1 + data = length 171, hash C786335F + sample 14: + time = 354000 + flags = 1 + data = length 161, hash 5E62A92D + sample 15: + time = 377000 + flags = 1 + data = length 168, hash E2E571E6 + sample 16: + time = 400000 + flags = 1 + data = length 167, hash DEF27757 + sample 17: + time = 423000 + flags = 1 + data = length 161, hash 2FA9D808 + sample 18: + time = 447000 + flags = 1 + data = length 165, hash 1C1800E0 + sample 19: + time = 470000 + flags = 1 + data = length 169, hash 2F971A34 + sample 20: + time = 493000 + flags = 1 + data = length 174, hash 1C1E47C4 + sample 21: + time = 516000 + flags = 1 + data = length 174, hash AEE91EC5 + sample 22: + time = 539000 + flags = 1 + data = length 171, hash 4A79E903 + sample 23: + time = 563000 + flags = 1 + data = length 173, hash 499BC474 + sample 24: + time = 586000 + flags = 1 + data = length 171, hash ED94C522 + sample 25: + time = 609000 + flags = 1 + data = length 170, hash 944F7760 + sample 26: + time = 633000 + flags = 1 + data = length 174, hash B3EAE626 + sample 27: + time = 656000 + flags = 1 + data = length 165, hash D52AC2F3 + sample 28: + time = 679000 + flags = 1 + data = length 167, hash 9E37502F + sample 29: + time = 703000 + flags = 1 + data = length 167, hash AC7FF7BE + sample 30: + time = 726000 + flags = 1 + data = length 169, hash 887355A9 + sample 31: + time = 749000 + flags = 1 + data = length 174, hash B85B8DAF + sample 32: + time = 772000 + flags = 1 + data = length 171, hash 99025912 + sample 33: + time = 795000 + flags = 1 + data = length 172, hash 63FAC2AB + sample 34: + time = 818000 + flags = 1 + data = length 175, hash CF626A45 + sample 35: + time = 842000 + flags = 1 + data = length 174, hash 23693E07 + sample 36: + time = 865000 + flags = 1 + data = length 166, hash 6CB9B957 + sample 37: + time = 888000 + flags = 1 + data = length 174, hash 60CAF38B + sample 38: + time = 911000 + flags = 1 + data = length 172, hash B8DB61E4 + sample 39: + time = 934000 + flags = 1 + data = length 169, hash 9172FCE2 + sample 40: + time = 957000 + flags = 1 + data = length 174, hash 30BB0142 + sample 41: + time = 981000 + flags = 1 + data = length 172, hash 2C84B20D + sample 42: + time = 1004000 + flags = 1 + data = length 168, hash 48C74EF + sample 43: + time = 1027000 + flags = 1 + data = length 166, hash 6986BFEA + sample 44: + time = 1050000 + flags = 1 + data = length 174, hash 54021595 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.1.dump b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.1.dump new file mode 100644 index 0000000000..b3cfcbe766 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.1.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1072000 + getPosition(0) = [[timeUs=0, position=4312]] + getPosition(1) = [[timeUs=0, position=4312]] + getPosition(536000) = [[timeUs=0, position=4312]] + getPosition(1072000) = [[timeUs=0, position=4312]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 7282 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/vorbis + maxInputSize = 8192 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash 71A77B76 + data = length 3189, hash 90EA712C + sample 0: + time = 59000 + flags = 1 + data = length 5, hash 1B4605F + sample 1: + time = 62000 + flags = 1 + data = length 24, hash 12FEB31D + sample 2: + time = 75000 + flags = 1 + data = length 139, hash 837FE175 + sample 3: + time = 98000 + flags = 1 + data = length 165, hash 9C52658 + sample 4: + time = 121000 + flags = 1 + data = length 166, hash 29F7D096 + sample 5: + time = 145000 + flags = 1 + data = length 162, hash 91BB916A + sample 6: + time = 168000 + flags = 1 + data = length 166, hash 42E61A0D + sample 7: + time = 191000 + flags = 1 + data = length 173, hash 69DCAA15 + sample 8: + time = 214000 + flags = 1 + data = length 171, hash 6BEB915E + sample 9: + time = 237000 + flags = 1 + data = length 162, hash 8580596B + sample 10: + time = 261000 + flags = 1 + data = length 174, hash 4561AB7E + sample 11: + time = 284000 + flags = 1 + data = length 169, hash E9CCB702 + sample 12: + time = 307000 + flags = 1 + data = length 168, hash 2C07206 + sample 13: + time = 330000 + flags = 1 + data = length 171, hash C786335F + sample 14: + time = 354000 + flags = 1 + data = length 161, hash 5E62A92D + sample 15: + time = 377000 + flags = 1 + data = length 168, hash E2E571E6 + sample 16: + time = 400000 + flags = 1 + data = length 167, hash DEF27757 + sample 17: + time = 423000 + flags = 1 + data = length 161, hash 2FA9D808 + sample 18: + time = 447000 + flags = 1 + data = length 165, hash 1C1800E0 + sample 19: + time = 470000 + flags = 1 + data = length 169, hash 2F971A34 + sample 20: + time = 493000 + flags = 1 + data = length 174, hash 1C1E47C4 + sample 21: + time = 516000 + flags = 1 + data = length 174, hash AEE91EC5 + sample 22: + time = 539000 + flags = 1 + data = length 171, hash 4A79E903 + sample 23: + time = 563000 + flags = 1 + data = length 173, hash 499BC474 + sample 24: + time = 586000 + flags = 1 + data = length 171, hash ED94C522 + sample 25: + time = 609000 + flags = 1 + data = length 170, hash 944F7760 + sample 26: + time = 633000 + flags = 1 + data = length 174, hash B3EAE626 + sample 27: + time = 656000 + flags = 1 + data = length 165, hash D52AC2F3 + sample 28: + time = 679000 + flags = 1 + data = length 167, hash 9E37502F + sample 29: + time = 703000 + flags = 1 + data = length 167, hash AC7FF7BE + sample 30: + time = 726000 + flags = 1 + data = length 169, hash 887355A9 + sample 31: + time = 749000 + flags = 1 + data = length 174, hash B85B8DAF + sample 32: + time = 772000 + flags = 1 + data = length 171, hash 99025912 + sample 33: + time = 795000 + flags = 1 + data = length 172, hash 63FAC2AB + sample 34: + time = 818000 + flags = 1 + data = length 175, hash CF626A45 + sample 35: + time = 842000 + flags = 1 + data = length 174, hash 23693E07 + sample 36: + time = 865000 + flags = 1 + data = length 166, hash 6CB9B957 + sample 37: + time = 888000 + flags = 1 + data = length 174, hash 60CAF38B + sample 38: + time = 911000 + flags = 1 + data = length 172, hash B8DB61E4 + sample 39: + time = 934000 + flags = 1 + data = length 169, hash 9172FCE2 + sample 40: + time = 957000 + flags = 1 + data = length 174, hash 30BB0142 + sample 41: + time = 981000 + flags = 1 + data = length 172, hash 2C84B20D + sample 42: + time = 1004000 + flags = 1 + data = length 168, hash 48C74EF + sample 43: + time = 1027000 + flags = 1 + data = length 166, hash 6986BFEA + sample 44: + time = 1050000 + flags = 1 + data = length 174, hash 54021595 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.2.dump b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.2.dump new file mode 100644 index 0000000000..b3cfcbe766 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.2.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1072000 + getPosition(0) = [[timeUs=0, position=4312]] + getPosition(1) = [[timeUs=0, position=4312]] + getPosition(536000) = [[timeUs=0, position=4312]] + getPosition(1072000) = [[timeUs=0, position=4312]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 7282 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/vorbis + maxInputSize = 8192 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash 71A77B76 + data = length 3189, hash 90EA712C + sample 0: + time = 59000 + flags = 1 + data = length 5, hash 1B4605F + sample 1: + time = 62000 + flags = 1 + data = length 24, hash 12FEB31D + sample 2: + time = 75000 + flags = 1 + data = length 139, hash 837FE175 + sample 3: + time = 98000 + flags = 1 + data = length 165, hash 9C52658 + sample 4: + time = 121000 + flags = 1 + data = length 166, hash 29F7D096 + sample 5: + time = 145000 + flags = 1 + data = length 162, hash 91BB916A + sample 6: + time = 168000 + flags = 1 + data = length 166, hash 42E61A0D + sample 7: + time = 191000 + flags = 1 + data = length 173, hash 69DCAA15 + sample 8: + time = 214000 + flags = 1 + data = length 171, hash 6BEB915E + sample 9: + time = 237000 + flags = 1 + data = length 162, hash 8580596B + sample 10: + time = 261000 + flags = 1 + data = length 174, hash 4561AB7E + sample 11: + time = 284000 + flags = 1 + data = length 169, hash E9CCB702 + sample 12: + time = 307000 + flags = 1 + data = length 168, hash 2C07206 + sample 13: + time = 330000 + flags = 1 + data = length 171, hash C786335F + sample 14: + time = 354000 + flags = 1 + data = length 161, hash 5E62A92D + sample 15: + time = 377000 + flags = 1 + data = length 168, hash E2E571E6 + sample 16: + time = 400000 + flags = 1 + data = length 167, hash DEF27757 + sample 17: + time = 423000 + flags = 1 + data = length 161, hash 2FA9D808 + sample 18: + time = 447000 + flags = 1 + data = length 165, hash 1C1800E0 + sample 19: + time = 470000 + flags = 1 + data = length 169, hash 2F971A34 + sample 20: + time = 493000 + flags = 1 + data = length 174, hash 1C1E47C4 + sample 21: + time = 516000 + flags = 1 + data = length 174, hash AEE91EC5 + sample 22: + time = 539000 + flags = 1 + data = length 171, hash 4A79E903 + sample 23: + time = 563000 + flags = 1 + data = length 173, hash 499BC474 + sample 24: + time = 586000 + flags = 1 + data = length 171, hash ED94C522 + sample 25: + time = 609000 + flags = 1 + data = length 170, hash 944F7760 + sample 26: + time = 633000 + flags = 1 + data = length 174, hash B3EAE626 + sample 27: + time = 656000 + flags = 1 + data = length 165, hash D52AC2F3 + sample 28: + time = 679000 + flags = 1 + data = length 167, hash 9E37502F + sample 29: + time = 703000 + flags = 1 + data = length 167, hash AC7FF7BE + sample 30: + time = 726000 + flags = 1 + data = length 169, hash 887355A9 + sample 31: + time = 749000 + flags = 1 + data = length 174, hash B85B8DAF + sample 32: + time = 772000 + flags = 1 + data = length 171, hash 99025912 + sample 33: + time = 795000 + flags = 1 + data = length 172, hash 63FAC2AB + sample 34: + time = 818000 + flags = 1 + data = length 175, hash CF626A45 + sample 35: + time = 842000 + flags = 1 + data = length 174, hash 23693E07 + sample 36: + time = 865000 + flags = 1 + data = length 166, hash 6CB9B957 + sample 37: + time = 888000 + flags = 1 + data = length 174, hash 60CAF38B + sample 38: + time = 911000 + flags = 1 + data = length 172, hash B8DB61E4 + sample 39: + time = 934000 + flags = 1 + data = length 169, hash 9172FCE2 + sample 40: + time = 957000 + flags = 1 + data = length 174, hash 30BB0142 + sample 41: + time = 981000 + flags = 1 + data = length 172, hash 2C84B20D + sample 42: + time = 1004000 + flags = 1 + data = length 168, hash 48C74EF + sample 43: + time = 1027000 + flags = 1 + data = length 166, hash 6986BFEA + sample 44: + time = 1050000 + flags = 1 + data = length 174, hash 54021595 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.3.dump b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.3.dump new file mode 100644 index 0000000000..b3cfcbe766 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.3.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1072000 + getPosition(0) = [[timeUs=0, position=4312]] + getPosition(1) = [[timeUs=0, position=4312]] + getPosition(536000) = [[timeUs=0, position=4312]] + getPosition(1072000) = [[timeUs=0, position=4312]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 7282 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/vorbis + maxInputSize = 8192 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash 71A77B76 + data = length 3189, hash 90EA712C + sample 0: + time = 59000 + flags = 1 + data = length 5, hash 1B4605F + sample 1: + time = 62000 + flags = 1 + data = length 24, hash 12FEB31D + sample 2: + time = 75000 + flags = 1 + data = length 139, hash 837FE175 + sample 3: + time = 98000 + flags = 1 + data = length 165, hash 9C52658 + sample 4: + time = 121000 + flags = 1 + data = length 166, hash 29F7D096 + sample 5: + time = 145000 + flags = 1 + data = length 162, hash 91BB916A + sample 6: + time = 168000 + flags = 1 + data = length 166, hash 42E61A0D + sample 7: + time = 191000 + flags = 1 + data = length 173, hash 69DCAA15 + sample 8: + time = 214000 + flags = 1 + data = length 171, hash 6BEB915E + sample 9: + time = 237000 + flags = 1 + data = length 162, hash 8580596B + sample 10: + time = 261000 + flags = 1 + data = length 174, hash 4561AB7E + sample 11: + time = 284000 + flags = 1 + data = length 169, hash E9CCB702 + sample 12: + time = 307000 + flags = 1 + data = length 168, hash 2C07206 + sample 13: + time = 330000 + flags = 1 + data = length 171, hash C786335F + sample 14: + time = 354000 + flags = 1 + data = length 161, hash 5E62A92D + sample 15: + time = 377000 + flags = 1 + data = length 168, hash E2E571E6 + sample 16: + time = 400000 + flags = 1 + data = length 167, hash DEF27757 + sample 17: + time = 423000 + flags = 1 + data = length 161, hash 2FA9D808 + sample 18: + time = 447000 + flags = 1 + data = length 165, hash 1C1800E0 + sample 19: + time = 470000 + flags = 1 + data = length 169, hash 2F971A34 + sample 20: + time = 493000 + flags = 1 + data = length 174, hash 1C1E47C4 + sample 21: + time = 516000 + flags = 1 + data = length 174, hash AEE91EC5 + sample 22: + time = 539000 + flags = 1 + data = length 171, hash 4A79E903 + sample 23: + time = 563000 + flags = 1 + data = length 173, hash 499BC474 + sample 24: + time = 586000 + flags = 1 + data = length 171, hash ED94C522 + sample 25: + time = 609000 + flags = 1 + data = length 170, hash 944F7760 + sample 26: + time = 633000 + flags = 1 + data = length 174, hash B3EAE626 + sample 27: + time = 656000 + flags = 1 + data = length 165, hash D52AC2F3 + sample 28: + time = 679000 + flags = 1 + data = length 167, hash 9E37502F + sample 29: + time = 703000 + flags = 1 + data = length 167, hash AC7FF7BE + sample 30: + time = 726000 + flags = 1 + data = length 169, hash 887355A9 + sample 31: + time = 749000 + flags = 1 + data = length 174, hash B85B8DAF + sample 32: + time = 772000 + flags = 1 + data = length 171, hash 99025912 + sample 33: + time = 795000 + flags = 1 + data = length 172, hash 63FAC2AB + sample 34: + time = 818000 + flags = 1 + data = length 175, hash CF626A45 + sample 35: + time = 842000 + flags = 1 + data = length 174, hash 23693E07 + sample 36: + time = 865000 + flags = 1 + data = length 166, hash 6CB9B957 + sample 37: + time = 888000 + flags = 1 + data = length 174, hash 60CAF38B + sample 38: + time = 911000 + flags = 1 + data = length 172, hash B8DB61E4 + sample 39: + time = 934000 + flags = 1 + data = length 169, hash 9172FCE2 + sample 40: + time = 957000 + flags = 1 + data = length 174, hash 30BB0142 + sample 41: + time = 981000 + flags = 1 + data = length 172, hash 2C84B20D + sample 42: + time = 1004000 + flags = 1 + data = length 168, hash 48C74EF + sample 43: + time = 1027000 + flags = 1 + data = length 166, hash 6986BFEA + sample 44: + time = 1050000 + flags = 1 + data = length 174, hash 54021595 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.unknown_length.dump b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.unknown_length.dump new file mode 100644 index 0000000000..b3cfcbe766 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/mkv/sample_with_vorbis_audio.mkv.unknown_length.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1072000 + getPosition(0) = [[timeUs=0, position=4312]] + getPosition(1) = [[timeUs=0, position=4312]] + getPosition(536000) = [[timeUs=0, position=4312]] + getPosition(1072000) = [[timeUs=0, position=4312]] +numberOfTracks = 2 +track 1: + total output bytes = 89502 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B + sample 0: + time = 0 + flags = 1 + data = length 36477, hash F0F36CFE + sample 1: + time = 67000 + flags = 0 + data = length 5341, hash 40B85E2 + sample 2: + time = 33000 + flags = 0 + data = length 596, hash 357B4D92 + sample 3: + time = 200000 + flags = 0 + data = length 7704, hash A39EDA06 + sample 4: + time = 133000 + flags = 0 + data = length 989, hash 2813C72D + sample 5: + time = 100000 + flags = 0 + data = length 721, hash C50D1C73 + sample 6: + time = 167000 + flags = 0 + data = length 519, hash 65FE1911 + sample 7: + time = 333000 + flags = 0 + data = length 6160, hash E1CAC0EC + sample 8: + time = 267000 + flags = 0 + data = length 953, hash 7160C661 + sample 9: + time = 233000 + flags = 0 + data = length 620, hash 7A7AE07C + sample 10: + time = 300000 + flags = 0 + data = length 405, hash 5CC7F4E7 + sample 11: + time = 433000 + flags = 0 + data = length 4852, hash 9DB6979D + sample 12: + time = 400000 + flags = 0 + data = length 547, hash E31A6979 + sample 13: + time = 367000 + flags = 0 + data = length 570, hash FEC40D00 + sample 14: + time = 567000 + flags = 0 + data = length 5525, hash 7C478F7E + sample 15: + time = 500000 + flags = 0 + data = length 1082, hash DA07059A + sample 16: + time = 467000 + flags = 0 + data = length 807, hash 93478E6B + sample 17: + time = 533000 + flags = 0 + data = length 744, hash 9A8E6026 + sample 18: + time = 700000 + flags = 0 + data = length 4732, hash C73B23C0 + sample 19: + time = 633000 + flags = 0 + data = length 1004, hash 8A19A228 + sample 20: + time = 600000 + flags = 0 + data = length 794, hash 8126022C + sample 21: + time = 667000 + flags = 0 + data = length 645, hash F08300E5 + sample 22: + time = 833000 + flags = 0 + data = length 2684, hash 727FE378 + sample 23: + time = 767000 + flags = 0 + data = length 787, hash 419A7821 + sample 24: + time = 733000 + flags = 0 + data = length 649, hash 5C159346 + sample 25: + time = 800000 + flags = 0 + data = length 509, hash F912D655 + sample 26: + time = 967000 + flags = 0 + data = length 1226, hash 29815C21 + sample 27: + time = 900000 + flags = 0 + data = length 898, hash D997AD0A + sample 28: + time = 867000 + flags = 0 + data = length 476, hash A0423645 + sample 29: + time = 933000 + flags = 0 + data = length 486, hash DDF32CBB +track 2: + total output bytes = 7282 + sample count = 45 + format 0: + id = 2 + sampleMimeType = audio/vorbis + maxInputSize = 8192 + channelCount = 1 + sampleRate = 44100 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash 71A77B76 + data = length 3189, hash 90EA712C + sample 0: + time = 59000 + flags = 1 + data = length 5, hash 1B4605F + sample 1: + time = 62000 + flags = 1 + data = length 24, hash 12FEB31D + sample 2: + time = 75000 + flags = 1 + data = length 139, hash 837FE175 + sample 3: + time = 98000 + flags = 1 + data = length 165, hash 9C52658 + sample 4: + time = 121000 + flags = 1 + data = length 166, hash 29F7D096 + sample 5: + time = 145000 + flags = 1 + data = length 162, hash 91BB916A + sample 6: + time = 168000 + flags = 1 + data = length 166, hash 42E61A0D + sample 7: + time = 191000 + flags = 1 + data = length 173, hash 69DCAA15 + sample 8: + time = 214000 + flags = 1 + data = length 171, hash 6BEB915E + sample 9: + time = 237000 + flags = 1 + data = length 162, hash 8580596B + sample 10: + time = 261000 + flags = 1 + data = length 174, hash 4561AB7E + sample 11: + time = 284000 + flags = 1 + data = length 169, hash E9CCB702 + sample 12: + time = 307000 + flags = 1 + data = length 168, hash 2C07206 + sample 13: + time = 330000 + flags = 1 + data = length 171, hash C786335F + sample 14: + time = 354000 + flags = 1 + data = length 161, hash 5E62A92D + sample 15: + time = 377000 + flags = 1 + data = length 168, hash E2E571E6 + sample 16: + time = 400000 + flags = 1 + data = length 167, hash DEF27757 + sample 17: + time = 423000 + flags = 1 + data = length 161, hash 2FA9D808 + sample 18: + time = 447000 + flags = 1 + data = length 165, hash 1C1800E0 + sample 19: + time = 470000 + flags = 1 + data = length 169, hash 2F971A34 + sample 20: + time = 493000 + flags = 1 + data = length 174, hash 1C1E47C4 + sample 21: + time = 516000 + flags = 1 + data = length 174, hash AEE91EC5 + sample 22: + time = 539000 + flags = 1 + data = length 171, hash 4A79E903 + sample 23: + time = 563000 + flags = 1 + data = length 173, hash 499BC474 + sample 24: + time = 586000 + flags = 1 + data = length 171, hash ED94C522 + sample 25: + time = 609000 + flags = 1 + data = length 170, hash 944F7760 + sample 26: + time = 633000 + flags = 1 + data = length 174, hash B3EAE626 + sample 27: + time = 656000 + flags = 1 + data = length 165, hash D52AC2F3 + sample 28: + time = 679000 + flags = 1 + data = length 167, hash 9E37502F + sample 29: + time = 703000 + flags = 1 + data = length 167, hash AC7FF7BE + sample 30: + time = 726000 + flags = 1 + data = length 169, hash 887355A9 + sample 31: + time = 749000 + flags = 1 + data = length 174, hash B85B8DAF + sample 32: + time = 772000 + flags = 1 + data = length 171, hash 99025912 + sample 33: + time = 795000 + flags = 1 + data = length 172, hash 63FAC2AB + sample 34: + time = 818000 + flags = 1 + data = length 175, hash CF626A45 + sample 35: + time = 842000 + flags = 1 + data = length 174, hash 23693E07 + sample 36: + time = 865000 + flags = 1 + data = length 166, hash 6CB9B957 + sample 37: + time = 888000 + flags = 1 + data = length 174, hash 60CAF38B + sample 38: + time = 911000 + flags = 1 + data = length 172, hash B8DB61E4 + sample 39: + time = 934000 + flags = 1 + data = length 169, hash 9172FCE2 + sample 40: + time = 957000 + flags = 1 + data = length 174, hash 30BB0142 + sample 41: + time = 981000 + flags = 1 + data = length 172, hash 2C84B20D + sample 42: + time = 1004000 + flags = 1 + data = length 168, hash 48C74EF + sample 43: + time = 1027000 + flags = 1 + data = length 166, hash 6986BFEA + sample 44: + time = 1050000 + flags = 1 + data = length 174, hash 54021595 +tracksEnded = true diff --git a/testdata/src/test/assets/media/jpeg/pixel-motion-photo-shortened.jpg b/testdata/src/test/assets/media/jpeg/pixel-motion-photo-shortened.jpg index 59d178d78c..082b9c6a0e 100644 Binary files a/testdata/src/test/assets/media/jpeg/pixel-motion-photo-shortened.jpg and b/testdata/src/test/assets/media/jpeg/pixel-motion-photo-shortened.jpg differ diff --git a/testdata/src/test/assets/media/jpeg/ss-motion-photo-shortened.jpg b/testdata/src/test/assets/media/jpeg/ss-motion-photo-shortened.jpg index 900fb481a2..12a74eec96 100644 Binary files a/testdata/src/test/assets/media/jpeg/ss-motion-photo-shortened.jpg and b/testdata/src/test/assets/media/jpeg/ss-motion-photo-shortened.jpg differ diff --git a/testdata/src/test/assets/media/mkv/sample_with_vorbis_audio.mkv b/testdata/src/test/assets/media/mkv/sample_with_vorbis_audio.mkv new file mode 100644 index 0000000000..1d0dbf57ca Binary files /dev/null and b/testdata/src/test/assets/media/mkv/sample_with_vorbis_audio.mkv differ diff --git a/testdata/src/test/assets/media/ssa/colors b/testdata/src/test/assets/media/ssa/colors new file mode 100644 index 0000000000..36ce7de761 --- /dev/null +++ b/testdata/src/test/assets/media/ssa/colors @@ -0,0 +1,26 @@ +[Script Info] +Title: Coloring +Script Type: V4.00+ +PlayResX: 1280 +PlayResY: 720 + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: PrimaryColourStyleHexRed ,Roboto,50,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 +Style: PrimaryColourStyleHexYellow ,Roboto,50,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 +Style: PrimaryColourStyleHexGreen ,Roboto,50,&HFF00 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 +Style: PrimaryColourStyleHexAlpha ,Roboto,50,&HA00000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 +Style: PrimaryColourStyleDecimal ,Roboto,50,16711680 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 +Style: PrimaryColourStyleDecimalAlpha ,Roboto,50,2164195328,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 +Style: PrimaryColourStyleInvalid ,Roboto,50,blue ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 + + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,Arnold,0,0,0,,First line in RED (&H000000FF). +Dialogue: 0,0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Arnold,0,0,0,,Second line in YELLOW (&H0000FFFF). +Dialogue: 0,0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,Third line in GREEN (leading zeros &HFF00). +Dialogue: 0,0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Arnold,0,0,0,,Fourth line in RED with alpha (&H400000FF). +Dialogue: 0,0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Arnold,0,0,0,,Fifth line in BLUE (16711680). +Dialogue: 0,0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha ,Arnold,0,0,0,,Sixth line in BLUE with alpha (2164195328). +Dialogue: 0,0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Arnold,0,0,0,,Seventh line with invalid color . diff --git a/testdata/src/test/assets/playbackdumps/playlists/bypass-off-then-on.dump b/testdata/src/test/assets/playbackdumps/playlists/bypass-off-then-on.dump new file mode 100644 index 0000000000..24a32498ee --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/playlists/bypass-off-then-on.dump @@ -0,0 +1,140 @@ +MediaCodecAdapter (exotest.audio.opus): + buffers.length = 138 + buffers[0] = length 375, hash 147EA9B + buffers[1] = length 187, hash C8ADD7C2 + buffers[2] = length 175, hash A6D94D6E + buffers[3] = length 162, hash 45359884 + buffers[4] = length 163, hash CBB836AF + buffers[5] = length 293, hash EEB23890 + buffers[6] = length 160, hash 7843AFDA + buffers[7] = length 162, hash 607E26A4 + buffers[8] = length 164, hash C1423D63 + buffers[9] = length 169, hash 90CEDF8C + buffers[10] = length 165, hash 97A6A3F7 + buffers[11] = length 179, hash 2EA2049F + buffers[12] = length 168, hash FCD51794 + buffers[13] = length 162, hash 80D9FBC0 + buffers[14] = length 162, hash BB673AC7 + buffers[15] = length 161, hash 8D5CC41B + buffers[16] = length 161, hash 5F5E6270 + buffers[17] = length 165, hash 117B14D9 + buffers[18] = length 166, hash D8BFD4 + buffers[19] = length 162, hash 61D76007 + buffers[20] = length 165, hash 78245BE8 + buffers[21] = length 165, hash A5F5B919 + buffers[22] = length 255, hash 1F42ECE2 + buffers[23] = length 165, hash D89D3EF0 + buffers[24] = length 164, hash C44C8E79 + buffers[25] = length 163, hash FFCE2E84 + buffers[26] = length 184, hash FD7BF02A + buffers[27] = length 162, hash 59074C0F + buffers[28] = length 162, hash 41CAF78D + buffers[29] = length 163, hash 50F0BCBD + buffers[30] = length 163, hash FABC49B3 + buffers[31] = length 256, hash 8515E521 + buffers[32] = length 244, hash D5F80618 + buffers[33] = length 162, hash A23FA880 + buffers[34] = length 163, hash 5D99DCD2 + buffers[35] = length 163, hash 37A4EB87 + buffers[36] = length 164, hash 4C190996 + buffers[37] = length 164, hash A2F6E788 + buffers[38] = length 162, hash E7353EFB + buffers[39] = length 161, hash FFF24D5F + buffers[40] = length 162, hash 95B27AB0 + buffers[41] = length 163, hash C43CB498 + buffers[42] = length 164, hash 438F5714 + buffers[43] = length 163, hash BDB72F57 + buffers[44] = length 162, hash 3194B57A + buffers[45] = length 163, hash D7CC025 + buffers[46] = length 162, hash F9E19F4D + buffers[47] = length 194, hash EED4C2BD + buffers[48] = length 164, hash ABFAEEFE + buffers[49] = length 163, hash 7487380A + buffers[50] = length 163, hash D4BFFB76 + buffers[51] = length 164, hash F3EB6797 + buffers[52] = length 163, hash 82B7ABB7 + buffers[53] = length 177, hash 921FEDAE + buffers[54] = length 162, hash BC7D176B + buffers[55] = length 165, hash 32DAEB04 + buffers[56] = length 164, hash 55FDBC77 + buffers[57] = length 230, hash FC32522D + buffers[58] = length 177, hash DF834667 + buffers[59] = length 161, hash F2ADFBCA + buffers[60] = length 161, hash 13CB7679 + buffers[61] = length 164, hash A12B20AC + buffers[62] = length 163, hash 38D448B + buffers[63] = length 164, hash BFE96C9A + buffers[64] = length 161, hash 921431E3 + buffers[65] = length 162, hash 9DDE27E0 + buffers[66] = length 165, hash 42C01110 + buffers[67] = length 163, hash C244C6B1 + buffers[68] = length 162, hash 288A7D7A + buffers[69] = length 164, hash 6DDF8E96 + buffers[70] = length 312, hash DD1760ED + buffers[71] = length 164, hash 40BD6AB0 + buffers[72] = length 167, hash 45FEB94 + buffers[73] = length 164, hash 1783D8D9 + buffers[74] = length 165, hash 7F68CB47 + buffers[75] = length 163, hash 431D98B9 + buffers[76] = length 164, hash 2F7F0A03 + buffers[77] = length 164, hash 330E9D40 + buffers[78] = length 161, hash 670A6D84 + buffers[79] = length 162, hash 55CEAB6A + buffers[80] = length 161, hash 690C1C44 + buffers[81] = length 311, hash 507DC3E7 + buffers[82] = length 226, hash 2D0C0942 + buffers[83] = length 163, hash 47A75060 + buffers[84] = length 163, hash 198A78EB + buffers[85] = length 165, hash F7AF184 + buffers[86] = length 163, hash 7EC009AE + buffers[87] = length 163, hash 7ACF600A + buffers[88] = length 170, hash 67F513C9 + buffers[89] = length 162, hash E0116535 + buffers[90] = length 164, hash 6C4C8BC1 + buffers[91] = length 163, hash 73E55623 + buffers[92] = length 162, hash 614AB0EE + buffers[93] = length 162, hash 49E038A6 + buffers[94] = length 162, hash 45BBCDDF + buffers[95] = length 163, hash 94E6047A + buffers[96] = length 162, hash FA40E646 + buffers[97] = length 163, hash 54F3E885 + buffers[98] = length 163, hash 42EA2C3C + buffers[99] = length 164, hash 11E5DC72 + buffers[100] = length 161, hash FB697FB7 + buffers[101] = length 164, hash 45137460 + buffers[102] = length 232, hash F8A33CF3 + buffers[103] = length 163, hash B2562537 + buffers[104] = length 163, hash D07ADBF + buffers[105] = length 163, hash 2AE2FC1E + buffers[106] = length 162, hash F574ABD + buffers[107] = length 162, hash 8A20D2FC + buffers[108] = length 162, hash BD37BF40 + buffers[109] = length 163, hash 81DF11E8 + buffers[110] = length 165, hash 236877C0 + buffers[111] = length 226, hash 6B5CD992 + buffers[112] = length 162, hash 7F697CCA + buffers[113] = length 161, hash 4C2993B4 + buffers[114] = length 163, hash 1DE49094 + buffers[115] = length 162, hash DCA5BB9B + buffers[116] = length 165, hash 66B62984 + buffers[117] = length 161, hash 994C6D54 + buffers[118] = length 163, hash DA5BA1F1 + buffers[119] = length 187, hash 7F6C5537 + buffers[120] = length 161, hash D0AF4628 + buffers[121] = length 161, hash 8A49A435 + buffers[122] = length 163, hash 90D7B180 + buffers[123] = length 162, hash C459D78E + buffers[124] = length 161, hash D7766E6B + buffers[125] = length 187, hash E0449F61 + buffers[126] = length 162, hash 203F238E + buffers[127] = length 163, hash 15F81805 + buffers[128] = length 161, hash 8496E779 + buffers[129] = length 163, hash DF6A28D0 + buffers[130] = length 233, hash 39CAC5CB + buffers[131] = length 250, hash 40F8863A + buffers[132] = length 248, hash BB880EB4 + buffers[133] = length 247, hash A93865FE + buffers[134] = length 244, hash ED7E6DB5 + buffers[135] = length 252, hash 2DD353C4 + buffers[136] = length 244, hash CE73B41E + buffers[137] = length 0, hash 1 diff --git a/testdata/src/test/assets/playbackdumps/playlists/bypass-on-then-off.dump b/testdata/src/test/assets/playbackdumps/playlists/bypass-on-then-off.dump new file mode 100644 index 0000000000..24a32498ee --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/playlists/bypass-on-then-off.dump @@ -0,0 +1,140 @@ +MediaCodecAdapter (exotest.audio.opus): + buffers.length = 138 + buffers[0] = length 375, hash 147EA9B + buffers[1] = length 187, hash C8ADD7C2 + buffers[2] = length 175, hash A6D94D6E + buffers[3] = length 162, hash 45359884 + buffers[4] = length 163, hash CBB836AF + buffers[5] = length 293, hash EEB23890 + buffers[6] = length 160, hash 7843AFDA + buffers[7] = length 162, hash 607E26A4 + buffers[8] = length 164, hash C1423D63 + buffers[9] = length 169, hash 90CEDF8C + buffers[10] = length 165, hash 97A6A3F7 + buffers[11] = length 179, hash 2EA2049F + buffers[12] = length 168, hash FCD51794 + buffers[13] = length 162, hash 80D9FBC0 + buffers[14] = length 162, hash BB673AC7 + buffers[15] = length 161, hash 8D5CC41B + buffers[16] = length 161, hash 5F5E6270 + buffers[17] = length 165, hash 117B14D9 + buffers[18] = length 166, hash D8BFD4 + buffers[19] = length 162, hash 61D76007 + buffers[20] = length 165, hash 78245BE8 + buffers[21] = length 165, hash A5F5B919 + buffers[22] = length 255, hash 1F42ECE2 + buffers[23] = length 165, hash D89D3EF0 + buffers[24] = length 164, hash C44C8E79 + buffers[25] = length 163, hash FFCE2E84 + buffers[26] = length 184, hash FD7BF02A + buffers[27] = length 162, hash 59074C0F + buffers[28] = length 162, hash 41CAF78D + buffers[29] = length 163, hash 50F0BCBD + buffers[30] = length 163, hash FABC49B3 + buffers[31] = length 256, hash 8515E521 + buffers[32] = length 244, hash D5F80618 + buffers[33] = length 162, hash A23FA880 + buffers[34] = length 163, hash 5D99DCD2 + buffers[35] = length 163, hash 37A4EB87 + buffers[36] = length 164, hash 4C190996 + buffers[37] = length 164, hash A2F6E788 + buffers[38] = length 162, hash E7353EFB + buffers[39] = length 161, hash FFF24D5F + buffers[40] = length 162, hash 95B27AB0 + buffers[41] = length 163, hash C43CB498 + buffers[42] = length 164, hash 438F5714 + buffers[43] = length 163, hash BDB72F57 + buffers[44] = length 162, hash 3194B57A + buffers[45] = length 163, hash D7CC025 + buffers[46] = length 162, hash F9E19F4D + buffers[47] = length 194, hash EED4C2BD + buffers[48] = length 164, hash ABFAEEFE + buffers[49] = length 163, hash 7487380A + buffers[50] = length 163, hash D4BFFB76 + buffers[51] = length 164, hash F3EB6797 + buffers[52] = length 163, hash 82B7ABB7 + buffers[53] = length 177, hash 921FEDAE + buffers[54] = length 162, hash BC7D176B + buffers[55] = length 165, hash 32DAEB04 + buffers[56] = length 164, hash 55FDBC77 + buffers[57] = length 230, hash FC32522D + buffers[58] = length 177, hash DF834667 + buffers[59] = length 161, hash F2ADFBCA + buffers[60] = length 161, hash 13CB7679 + buffers[61] = length 164, hash A12B20AC + buffers[62] = length 163, hash 38D448B + buffers[63] = length 164, hash BFE96C9A + buffers[64] = length 161, hash 921431E3 + buffers[65] = length 162, hash 9DDE27E0 + buffers[66] = length 165, hash 42C01110 + buffers[67] = length 163, hash C244C6B1 + buffers[68] = length 162, hash 288A7D7A + buffers[69] = length 164, hash 6DDF8E96 + buffers[70] = length 312, hash DD1760ED + buffers[71] = length 164, hash 40BD6AB0 + buffers[72] = length 167, hash 45FEB94 + buffers[73] = length 164, hash 1783D8D9 + buffers[74] = length 165, hash 7F68CB47 + buffers[75] = length 163, hash 431D98B9 + buffers[76] = length 164, hash 2F7F0A03 + buffers[77] = length 164, hash 330E9D40 + buffers[78] = length 161, hash 670A6D84 + buffers[79] = length 162, hash 55CEAB6A + buffers[80] = length 161, hash 690C1C44 + buffers[81] = length 311, hash 507DC3E7 + buffers[82] = length 226, hash 2D0C0942 + buffers[83] = length 163, hash 47A75060 + buffers[84] = length 163, hash 198A78EB + buffers[85] = length 165, hash F7AF184 + buffers[86] = length 163, hash 7EC009AE + buffers[87] = length 163, hash 7ACF600A + buffers[88] = length 170, hash 67F513C9 + buffers[89] = length 162, hash E0116535 + buffers[90] = length 164, hash 6C4C8BC1 + buffers[91] = length 163, hash 73E55623 + buffers[92] = length 162, hash 614AB0EE + buffers[93] = length 162, hash 49E038A6 + buffers[94] = length 162, hash 45BBCDDF + buffers[95] = length 163, hash 94E6047A + buffers[96] = length 162, hash FA40E646 + buffers[97] = length 163, hash 54F3E885 + buffers[98] = length 163, hash 42EA2C3C + buffers[99] = length 164, hash 11E5DC72 + buffers[100] = length 161, hash FB697FB7 + buffers[101] = length 164, hash 45137460 + buffers[102] = length 232, hash F8A33CF3 + buffers[103] = length 163, hash B2562537 + buffers[104] = length 163, hash D07ADBF + buffers[105] = length 163, hash 2AE2FC1E + buffers[106] = length 162, hash F574ABD + buffers[107] = length 162, hash 8A20D2FC + buffers[108] = length 162, hash BD37BF40 + buffers[109] = length 163, hash 81DF11E8 + buffers[110] = length 165, hash 236877C0 + buffers[111] = length 226, hash 6B5CD992 + buffers[112] = length 162, hash 7F697CCA + buffers[113] = length 161, hash 4C2993B4 + buffers[114] = length 163, hash 1DE49094 + buffers[115] = length 162, hash DCA5BB9B + buffers[116] = length 165, hash 66B62984 + buffers[117] = length 161, hash 994C6D54 + buffers[118] = length 163, hash DA5BA1F1 + buffers[119] = length 187, hash 7F6C5537 + buffers[120] = length 161, hash D0AF4628 + buffers[121] = length 161, hash 8A49A435 + buffers[122] = length 163, hash 90D7B180 + buffers[123] = length 162, hash C459D78E + buffers[124] = length 161, hash D7766E6B + buffers[125] = length 187, hash E0449F61 + buffers[126] = length 162, hash 203F238E + buffers[127] = length 163, hash 15F81805 + buffers[128] = length 161, hash 8496E779 + buffers[129] = length 163, hash DF6A28D0 + buffers[130] = length 233, hash 39CAC5CB + buffers[131] = length 250, hash 40F8863A + buffers[132] = length 248, hash BB880EB4 + buffers[133] = length 247, hash A93865FE + buffers[134] = length 244, hash ED7E6DB5 + buffers[135] = length 252, hash 2DD353C4 + buffers[136] = length 244, hash CE73B41E + buffers[137] = length 0, hash 1 diff --git a/testdata/src/test/assets/playbackdumps/wav/sample.wav.dump b/testdata/src/test/assets/playbackdumps/wav/sample.wav.dump new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testdata/src/test/assets/playbackdumps/wav/sample_ima_adpcm.wav.dump b/testdata/src/test/assets/playbackdumps/wav/sample_ima_adpcm.wav.dump new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testdata/src/test/assets/playbackdumps/wav/sample_with_trailing_bytes.wav.dump b/testdata/src/test/assets/playbackdumps/wav/sample_with_trailing_bytes.wav.dump new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump new file mode 100644 index 0000000000..c18193c16f --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/amr/sample_nb.amr.dump @@ -0,0 +1,1097 @@ +containerMimeType = video/mp4 +format 0: + sampleMimeType = audio/3gpp + channelCount = 1 + sampleRate = 8000 + pcmEncoding = 2 +sample: + trackIndex = 0 + dataHashCode = 924517484 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = -835666085 + isKeyFrame = true + presentationTimeUs = 750 +sample: + trackIndex = 0 + dataHashCode = 430283125 + isKeyFrame = true + presentationTimeUs = 1500 +sample: + trackIndex = 0 + dataHashCode = 1215919932 + isKeyFrame = true + presentationTimeUs = 2250 +sample: + trackIndex = 0 + dataHashCode = -386387943 + isKeyFrame = true + presentationTimeUs = 3000 +sample: + trackIndex = 0 + dataHashCode = -765080119 + isKeyFrame = true + presentationTimeUs = 3750 +sample: + trackIndex = 0 + dataHashCode = -1855636054 + isKeyFrame = true + presentationTimeUs = 4500 +sample: + trackIndex = 0 + dataHashCode = -946579722 + isKeyFrame = true + presentationTimeUs = 5250 +sample: + trackIndex = 0 + dataHashCode = -841202654 + isKeyFrame = true + presentationTimeUs = 6000 +sample: + trackIndex = 0 + dataHashCode = -638764303 + isKeyFrame = true + presentationTimeUs = 6750 +sample: + trackIndex = 0 + dataHashCode = -1162388941 + isKeyFrame = true + presentationTimeUs = 7500 +sample: + trackIndex = 0 + dataHashCode = 572634367 + isKeyFrame = true + presentationTimeUs = 8250 +sample: + trackIndex = 0 + dataHashCode = -1774188021 + isKeyFrame = true + presentationTimeUs = 9000 +sample: + trackIndex = 0 + dataHashCode = 92464891 + isKeyFrame = true + presentationTimeUs = 9750 +sample: + trackIndex = 0 + dataHashCode = -991397659 + isKeyFrame = true + presentationTimeUs = 10500 +sample: + trackIndex = 0 + dataHashCode = -934698563 + isKeyFrame = true + presentationTimeUs = 11250 +sample: + trackIndex = 0 + dataHashCode = -811030035 + isKeyFrame = true + presentationTimeUs = 12000 +sample: + trackIndex = 0 + dataHashCode = 1892305159 + isKeyFrame = true + presentationTimeUs = 12750 +sample: + trackIndex = 0 + dataHashCode = -1266858924 + isKeyFrame = true + presentationTimeUs = 13500 +sample: + trackIndex = 0 + dataHashCode = 673814721 + isKeyFrame = true + presentationTimeUs = 14250 +sample: + trackIndex = 0 + dataHashCode = 1061124709 + isKeyFrame = true + presentationTimeUs = 15000 +sample: + trackIndex = 0 + dataHashCode = -869356712 + isKeyFrame = true + presentationTimeUs = 15750 +sample: + trackIndex = 0 + dataHashCode = 664729362 + isKeyFrame = true + presentationTimeUs = 16500 +sample: + trackIndex = 0 + dataHashCode = -1439741143 + isKeyFrame = true + presentationTimeUs = 17250 +sample: + trackIndex = 0 + dataHashCode = -151627580 + isKeyFrame = true + presentationTimeUs = 18000 +sample: + trackIndex = 0 + dataHashCode = -673268457 + isKeyFrame = true + presentationTimeUs = 18750 +sample: + trackIndex = 0 + dataHashCode = 1839962647 + isKeyFrame = true + presentationTimeUs = 19500 +sample: + trackIndex = 0 + dataHashCode = 1858999665 + isKeyFrame = true + presentationTimeUs = 20250 +sample: + trackIndex = 0 + dataHashCode = -1278193537 + isKeyFrame = true + presentationTimeUs = 21000 +sample: + trackIndex = 0 + dataHashCode = 568547001 + isKeyFrame = true + presentationTimeUs = 21750 +sample: + trackIndex = 0 + dataHashCode = 68217362 + isKeyFrame = true + presentationTimeUs = 22500 +sample: + trackIndex = 0 + dataHashCode = 1396217256 + isKeyFrame = true + presentationTimeUs = 23250 +sample: + trackIndex = 0 + dataHashCode = -971293094 + isKeyFrame = true + presentationTimeUs = 24000 +sample: + trackIndex = 0 + dataHashCode = -1742638874 + isKeyFrame = true + presentationTimeUs = 24750 +sample: + trackIndex = 0 + dataHashCode = 2047109317 + isKeyFrame = true + presentationTimeUs = 25500 +sample: + trackIndex = 0 + dataHashCode = -1668945241 + isKeyFrame = true + presentationTimeUs = 26250 +sample: + trackIndex = 0 + dataHashCode = -1229766218 + isKeyFrame = true + presentationTimeUs = 27000 +sample: + trackIndex = 0 + dataHashCode = 1765233454 + isKeyFrame = true + presentationTimeUs = 27750 +sample: + trackIndex = 0 + dataHashCode = -1930255456 + isKeyFrame = true + presentationTimeUs = 28500 +sample: + trackIndex = 0 + dataHashCode = -764925242 + isKeyFrame = true + presentationTimeUs = 29250 +sample: + trackIndex = 0 + dataHashCode = -1144688369 + isKeyFrame = true + presentationTimeUs = 30000 +sample: + trackIndex = 0 + dataHashCode = 1493699436 + isKeyFrame = true + presentationTimeUs = 30750 +sample: + trackIndex = 0 + dataHashCode = -468614511 + isKeyFrame = true + presentationTimeUs = 31500 +sample: + trackIndex = 0 + dataHashCode = -1578782058 + isKeyFrame = true + presentationTimeUs = 32250 +sample: + trackIndex = 0 + dataHashCode = -675743397 + isKeyFrame = true + presentationTimeUs = 33000 +sample: + trackIndex = 0 + dataHashCode = -863790111 + isKeyFrame = true + presentationTimeUs = 33750 +sample: + trackIndex = 0 + dataHashCode = -732307506 + isKeyFrame = true + presentationTimeUs = 34500 +sample: + trackIndex = 0 + dataHashCode = -693298708 + isKeyFrame = true + presentationTimeUs = 35250 +sample: + trackIndex = 0 + dataHashCode = -799131843 + isKeyFrame = true + presentationTimeUs = 36000 +sample: + trackIndex = 0 + dataHashCode = 1782866119 + isKeyFrame = true + presentationTimeUs = 36750 +sample: + trackIndex = 0 + dataHashCode = -912205505 + isKeyFrame = true + presentationTimeUs = 37500 +sample: + trackIndex = 0 + dataHashCode = 1067981287 + isKeyFrame = true + presentationTimeUs = 38250 +sample: + trackIndex = 0 + dataHashCode = 490520060 + isKeyFrame = true + presentationTimeUs = 39000 +sample: + trackIndex = 0 + dataHashCode = -1950632957 + isKeyFrame = true + presentationTimeUs = 39750 +sample: + trackIndex = 0 + dataHashCode = 565485817 + isKeyFrame = true + presentationTimeUs = 40500 +sample: + trackIndex = 0 + dataHashCode = -1057414703 + isKeyFrame = true + presentationTimeUs = 41250 +sample: + trackIndex = 0 + dataHashCode = 1568746155 + isKeyFrame = true + presentationTimeUs = 42000 +sample: + trackIndex = 0 + dataHashCode = 1355412472 + isKeyFrame = true + presentationTimeUs = 42750 +sample: + trackIndex = 0 + dataHashCode = 1546368465 + isKeyFrame = true + presentationTimeUs = 43500 +sample: + trackIndex = 0 + dataHashCode = 1811529381 + isKeyFrame = true + presentationTimeUs = 44250 +sample: + trackIndex = 0 + dataHashCode = 658031078 + isKeyFrame = true + presentationTimeUs = 45000 +sample: + trackIndex = 0 + dataHashCode = 1606584486 + isKeyFrame = true + presentationTimeUs = 45750 +sample: + trackIndex = 0 + dataHashCode = 2123252778 + isKeyFrame = true + presentationTimeUs = 46500 +sample: + trackIndex = 0 + dataHashCode = -1364579398 + isKeyFrame = true + presentationTimeUs = 47250 +sample: + trackIndex = 0 + dataHashCode = 1311427887 + isKeyFrame = true + presentationTimeUs = 48000 +sample: + trackIndex = 0 + dataHashCode = -691467569 + isKeyFrame = true + presentationTimeUs = 48750 +sample: + trackIndex = 0 + dataHashCode = 1876470084 + isKeyFrame = true + presentationTimeUs = 49500 +sample: + trackIndex = 0 + dataHashCode = -1472873479 + isKeyFrame = true + presentationTimeUs = 50250 +sample: + trackIndex = 0 + dataHashCode = -143574992 + isKeyFrame = true + presentationTimeUs = 51000 +sample: + trackIndex = 0 + dataHashCode = 984180453 + isKeyFrame = true + presentationTimeUs = 51750 +sample: + trackIndex = 0 + dataHashCode = -113645527 + isKeyFrame = true + presentationTimeUs = 52500 +sample: + trackIndex = 0 + dataHashCode = 1987501641 + isKeyFrame = true + presentationTimeUs = 53250 +sample: + trackIndex = 0 + dataHashCode = -1816426230 + isKeyFrame = true + presentationTimeUs = 54000 +sample: + trackIndex = 0 + dataHashCode = -1250050360 + isKeyFrame = true + presentationTimeUs = 54750 +sample: + trackIndex = 0 + dataHashCode = 1722852790 + isKeyFrame = true + presentationTimeUs = 55500 +sample: + trackIndex = 0 + dataHashCode = 225656333 + isKeyFrame = true + presentationTimeUs = 56250 +sample: + trackIndex = 0 + dataHashCode = -2137778394 + isKeyFrame = true + presentationTimeUs = 57000 +sample: + trackIndex = 0 + dataHashCode = 1433327155 + isKeyFrame = true + presentationTimeUs = 57750 +sample: + trackIndex = 0 + dataHashCode = -974261023 + isKeyFrame = true + presentationTimeUs = 58500 +sample: + trackIndex = 0 + dataHashCode = 1797813317 + isKeyFrame = true + presentationTimeUs = 59250 +sample: + trackIndex = 0 + dataHashCode = -594033497 + isKeyFrame = true + presentationTimeUs = 60000 +sample: + trackIndex = 0 + dataHashCode = -628310540 + isKeyFrame = true + presentationTimeUs = 60750 +sample: + trackIndex = 0 + dataHashCode = 1868627831 + isKeyFrame = true + presentationTimeUs = 61500 +sample: + trackIndex = 0 + dataHashCode = 1051863958 + isKeyFrame = true + presentationTimeUs = 62250 +sample: + trackIndex = 0 + dataHashCode = -1279059211 + isKeyFrame = true + presentationTimeUs = 63000 +sample: + trackIndex = 0 + dataHashCode = 408201874 + isKeyFrame = true + presentationTimeUs = 63750 +sample: + trackIndex = 0 + dataHashCode = 1686644299 + isKeyFrame = true + presentationTimeUs = 64500 +sample: + trackIndex = 0 + dataHashCode = 1288226241 + isKeyFrame = true + presentationTimeUs = 65250 +sample: + trackIndex = 0 + dataHashCode = 432829731 + isKeyFrame = true + presentationTimeUs = 66000 +sample: + trackIndex = 0 + dataHashCode = -1679312600 + isKeyFrame = true + presentationTimeUs = 66750 +sample: + trackIndex = 0 + dataHashCode = 1206680829 + isKeyFrame = true + presentationTimeUs = 67500 +sample: + trackIndex = 0 + dataHashCode = -325844704 + isKeyFrame = true + presentationTimeUs = 68250 +sample: + trackIndex = 0 + dataHashCode = 1941808848 + isKeyFrame = true + presentationTimeUs = 69000 +sample: + trackIndex = 0 + dataHashCode = -87346412 + isKeyFrame = true + presentationTimeUs = 69750 +sample: + trackIndex = 0 + dataHashCode = -329133765 + isKeyFrame = true + presentationTimeUs = 70500 +sample: + trackIndex = 0 + dataHashCode = -1299416212 + isKeyFrame = true + presentationTimeUs = 71250 +sample: + trackIndex = 0 + dataHashCode = -1314599219 + isKeyFrame = true + presentationTimeUs = 72000 +sample: + trackIndex = 0 + dataHashCode = 1456741286 + isKeyFrame = true + presentationTimeUs = 72750 +sample: + trackIndex = 0 + dataHashCode = 151296500 + isKeyFrame = true + presentationTimeUs = 73500 +sample: + trackIndex = 0 + dataHashCode = 1708763603 + isKeyFrame = true + presentationTimeUs = 74250 +sample: + trackIndex = 0 + dataHashCode = 227542220 + isKeyFrame = true + presentationTimeUs = 75000 +sample: + trackIndex = 0 + dataHashCode = 1094305517 + isKeyFrame = true + presentationTimeUs = 75750 +sample: + trackIndex = 0 + dataHashCode = -990377604 + isKeyFrame = true + presentationTimeUs = 76500 +sample: + trackIndex = 0 + dataHashCode = -1798036230 + isKeyFrame = true + presentationTimeUs = 77250 +sample: + trackIndex = 0 + dataHashCode = -1027148291 + isKeyFrame = true + presentationTimeUs = 78000 +sample: + trackIndex = 0 + dataHashCode = 359763976 + isKeyFrame = true + presentationTimeUs = 78750 +sample: + trackIndex = 0 + dataHashCode = 1332016420 + isKeyFrame = true + presentationTimeUs = 79500 +sample: + trackIndex = 0 + dataHashCode = -102753250 + isKeyFrame = true + presentationTimeUs = 80250 +sample: + trackIndex = 0 + dataHashCode = 1959063156 + isKeyFrame = true + presentationTimeUs = 81000 +sample: + trackIndex = 0 + dataHashCode = 2129089853 + isKeyFrame = true + presentationTimeUs = 81750 +sample: + trackIndex = 0 + dataHashCode = 1658742073 + isKeyFrame = true + presentationTimeUs = 82500 +sample: + trackIndex = 0 + dataHashCode = 2136916514 + isKeyFrame = true + presentationTimeUs = 83250 +sample: + trackIndex = 0 + dataHashCode = 105121407 + isKeyFrame = true + presentationTimeUs = 84000 +sample: + trackIndex = 0 + dataHashCode = -839464484 + isKeyFrame = true + presentationTimeUs = 84750 +sample: + trackIndex = 0 + dataHashCode = -1956791168 + isKeyFrame = true + presentationTimeUs = 85500 +sample: + trackIndex = 0 + dataHashCode = -1387546109 + isKeyFrame = true + presentationTimeUs = 86250 +sample: + trackIndex = 0 + dataHashCode = 128410432 + isKeyFrame = true + presentationTimeUs = 87000 +sample: + trackIndex = 0 + dataHashCode = 907081136 + isKeyFrame = true + presentationTimeUs = 87750 +sample: + trackIndex = 0 + dataHashCode = 1124845067 + isKeyFrame = true + presentationTimeUs = 88500 +sample: + trackIndex = 0 + dataHashCode = -1714479962 + isKeyFrame = true + presentationTimeUs = 89250 +sample: + trackIndex = 0 + dataHashCode = 322029323 + isKeyFrame = true + presentationTimeUs = 90000 +sample: + trackIndex = 0 + dataHashCode = -1116281187 + isKeyFrame = true + presentationTimeUs = 90750 +sample: + trackIndex = 0 + dataHashCode = 1571181228 + isKeyFrame = true + presentationTimeUs = 91500 +sample: + trackIndex = 0 + dataHashCode = 997979854 + isKeyFrame = true + presentationTimeUs = 92250 +sample: + trackIndex = 0 + dataHashCode = -1413492413 + isKeyFrame = true + presentationTimeUs = 93000 +sample: + trackIndex = 0 + dataHashCode = -381390490 + isKeyFrame = true + presentationTimeUs = 93750 +sample: + trackIndex = 0 + dataHashCode = -331348340 + isKeyFrame = true + presentationTimeUs = 94500 +sample: + trackIndex = 0 + dataHashCode = -1568238592 + isKeyFrame = true + presentationTimeUs = 95250 +sample: + trackIndex = 0 + dataHashCode = -941591445 + isKeyFrame = true + presentationTimeUs = 96000 +sample: + trackIndex = 0 + dataHashCode = 1616911281 + isKeyFrame = true + presentationTimeUs = 96750 +sample: + trackIndex = 0 + dataHashCode = -1755664741 + isKeyFrame = true + presentationTimeUs = 97500 +sample: + trackIndex = 0 + dataHashCode = -1950609742 + isKeyFrame = true + presentationTimeUs = 98250 +sample: + trackIndex = 0 + dataHashCode = 1476082149 + isKeyFrame = true + presentationTimeUs = 99000 +sample: + trackIndex = 0 + dataHashCode = 1289547483 + isKeyFrame = true + presentationTimeUs = 99750 +sample: + trackIndex = 0 + dataHashCode = -367599018 + isKeyFrame = true + presentationTimeUs = 100500 +sample: + trackIndex = 0 + dataHashCode = 679378334 + isKeyFrame = true + presentationTimeUs = 101250 +sample: + trackIndex = 0 + dataHashCode = 1437306809 + isKeyFrame = true + presentationTimeUs = 102000 +sample: + trackIndex = 0 + dataHashCode = 311988463 + isKeyFrame = true + presentationTimeUs = 102750 +sample: + trackIndex = 0 + dataHashCode = -1870442665 + isKeyFrame = true + presentationTimeUs = 103500 +sample: + trackIndex = 0 + dataHashCode = 1530013920 + isKeyFrame = true + presentationTimeUs = 104250 +sample: + trackIndex = 0 + dataHashCode = -585506443 + isKeyFrame = true + presentationTimeUs = 105000 +sample: + trackIndex = 0 + dataHashCode = -293690558 + isKeyFrame = true + presentationTimeUs = 105750 +sample: + trackIndex = 0 + dataHashCode = -616893325 + isKeyFrame = true + presentationTimeUs = 106500 +sample: + trackIndex = 0 + dataHashCode = 632210495 + isKeyFrame = true + presentationTimeUs = 107250 +sample: + trackIndex = 0 + dataHashCode = -291767937 + isKeyFrame = true + presentationTimeUs = 108000 +sample: + trackIndex = 0 + dataHashCode = -270265 + isKeyFrame = true + presentationTimeUs = 108750 +sample: + trackIndex = 0 + dataHashCode = -1095959376 + isKeyFrame = true + presentationTimeUs = 109500 +sample: + trackIndex = 0 + dataHashCode = -1363867284 + isKeyFrame = true + presentationTimeUs = 110250 +sample: + trackIndex = 0 + dataHashCode = 185415707 + isKeyFrame = true + presentationTimeUs = 111000 +sample: + trackIndex = 0 + dataHashCode = 1033720098 + isKeyFrame = true + presentationTimeUs = 111750 +sample: + trackIndex = 0 + dataHashCode = 1813896085 + isKeyFrame = true + presentationTimeUs = 112500 +sample: + trackIndex = 0 + dataHashCode = -1381192241 + isKeyFrame = true + presentationTimeUs = 113250 +sample: + trackIndex = 0 + dataHashCode = 362689054 + isKeyFrame = true + presentationTimeUs = 114000 +sample: + trackIndex = 0 + dataHashCode = -1320787356 + isKeyFrame = true + presentationTimeUs = 114750 +sample: + trackIndex = 0 + dataHashCode = 1306489379 + isKeyFrame = true + presentationTimeUs = 115500 +sample: + trackIndex = 0 + dataHashCode = -910313430 + isKeyFrame = true + presentationTimeUs = 116250 +sample: + trackIndex = 0 + dataHashCode = -1533334115 + isKeyFrame = true + presentationTimeUs = 117000 +sample: + trackIndex = 0 + dataHashCode = -700061723 + isKeyFrame = true + presentationTimeUs = 117750 +sample: + trackIndex = 0 + dataHashCode = 474100444 + isKeyFrame = true + presentationTimeUs = 118500 +sample: + trackIndex = 0 + dataHashCode = -2096659943 + isKeyFrame = true + presentationTimeUs = 119250 +sample: + trackIndex = 0 + dataHashCode = -690442126 + isKeyFrame = true + presentationTimeUs = 120000 +sample: + trackIndex = 0 + dataHashCode = 158718784 + isKeyFrame = true + presentationTimeUs = 120750 +sample: + trackIndex = 0 + dataHashCode = -1587553019 + isKeyFrame = true + presentationTimeUs = 121500 +sample: + trackIndex = 0 + dataHashCode = 1266916929 + isKeyFrame = true + presentationTimeUs = 122250 +sample: + trackIndex = 0 + dataHashCode = 1947792537 + isKeyFrame = true + presentationTimeUs = 123000 +sample: + trackIndex = 0 + dataHashCode = 2051622372 + isKeyFrame = true + presentationTimeUs = 123750 +sample: + trackIndex = 0 + dataHashCode = 1648973196 + isKeyFrame = true + presentationTimeUs = 124500 +sample: + trackIndex = 0 + dataHashCode = -1119069213 + isKeyFrame = true + presentationTimeUs = 125250 +sample: + trackIndex = 0 + dataHashCode = -1162670307 + isKeyFrame = true + presentationTimeUs = 126000 +sample: + trackIndex = 0 + dataHashCode = 505180178 + isKeyFrame = true + presentationTimeUs = 126750 +sample: + trackIndex = 0 + dataHashCode = -1707111799 + isKeyFrame = true + presentationTimeUs = 127500 +sample: + trackIndex = 0 + dataHashCode = 549350779 + isKeyFrame = true + presentationTimeUs = 128250 +sample: + trackIndex = 0 + dataHashCode = -895461091 + isKeyFrame = true + presentationTimeUs = 129000 +sample: + trackIndex = 0 + dataHashCode = 1834306839 + isKeyFrame = true + presentationTimeUs = 129750 +sample: + trackIndex = 0 + dataHashCode = -646169807 + isKeyFrame = true + presentationTimeUs = 130500 +sample: + trackIndex = 0 + dataHashCode = 123454915 + isKeyFrame = true + presentationTimeUs = 131250 +sample: + trackIndex = 0 + dataHashCode = 2074179659 + isKeyFrame = true + presentationTimeUs = 132000 +sample: + trackIndex = 0 + dataHashCode = 488070546 + isKeyFrame = true + presentationTimeUs = 132750 +sample: + trackIndex = 0 + dataHashCode = -1379245827 + isKeyFrame = true + presentationTimeUs = 133500 +sample: + trackIndex = 0 + dataHashCode = 922846867 + isKeyFrame = true + presentationTimeUs = 134250 +sample: + trackIndex = 0 + dataHashCode = 1163092079 + isKeyFrame = true + presentationTimeUs = 135000 +sample: + trackIndex = 0 + dataHashCode = -817674907 + isKeyFrame = true + presentationTimeUs = 135750 +sample: + trackIndex = 0 + dataHashCode = -765143209 + isKeyFrame = true + presentationTimeUs = 136500 +sample: + trackIndex = 0 + dataHashCode = 1337234415 + isKeyFrame = true + presentationTimeUs = 137250 +sample: + trackIndex = 0 + dataHashCode = 152696122 + isKeyFrame = true + presentationTimeUs = 138000 +sample: + trackIndex = 0 + dataHashCode = -1037369189 + isKeyFrame = true + presentationTimeUs = 138750 +sample: + trackIndex = 0 + dataHashCode = 93852784 + isKeyFrame = true + presentationTimeUs = 139500 +sample: + trackIndex = 0 + dataHashCode = -1512860804 + isKeyFrame = true + presentationTimeUs = 140250 +sample: + trackIndex = 0 + dataHashCode = -1571797975 + isKeyFrame = true + presentationTimeUs = 141000 +sample: + trackIndex = 0 + dataHashCode = -1390710594 + isKeyFrame = true + presentationTimeUs = 141750 +sample: + trackIndex = 0 + dataHashCode = 775548254 + isKeyFrame = true + presentationTimeUs = 142500 +sample: + trackIndex = 0 + dataHashCode = 329825934 + isKeyFrame = true + presentationTimeUs = 143250 +sample: + trackIndex = 0 + dataHashCode = 449672203 + isKeyFrame = true + presentationTimeUs = 144000 +sample: + trackIndex = 0 + dataHashCode = 135215283 + isKeyFrame = true + presentationTimeUs = 144750 +sample: + trackIndex = 0 + dataHashCode = -627202145 + isKeyFrame = true + presentationTimeUs = 145500 +sample: + trackIndex = 0 + dataHashCode = 565795710 + isKeyFrame = true + presentationTimeUs = 146250 +sample: + trackIndex = 0 + dataHashCode = -853390981 + isKeyFrame = true + presentationTimeUs = 147000 +sample: + trackIndex = 0 + dataHashCode = 1904980829 + isKeyFrame = true + presentationTimeUs = 147750 +sample: + trackIndex = 0 + dataHashCode = 1772857005 + isKeyFrame = true + presentationTimeUs = 148500 +sample: + trackIndex = 0 + dataHashCode = -1159621303 + isKeyFrame = true + presentationTimeUs = 149250 +sample: + trackIndex = 0 + dataHashCode = 712585139 + isKeyFrame = true + presentationTimeUs = 150000 +sample: + trackIndex = 0 + dataHashCode = 7470296 + isKeyFrame = true + presentationTimeUs = 150750 +sample: + trackIndex = 0 + dataHashCode = 1154659763 + isKeyFrame = true + presentationTimeUs = 151500 +sample: + trackIndex = 0 + dataHashCode = 512209179 + isKeyFrame = true + presentationTimeUs = 152250 +sample: + trackIndex = 0 + dataHashCode = 2026712081 + isKeyFrame = true + presentationTimeUs = 153000 +sample: + trackIndex = 0 + dataHashCode = -1625715216 + isKeyFrame = true + presentationTimeUs = 153750 +sample: + trackIndex = 0 + dataHashCode = -1299058326 + isKeyFrame = true + presentationTimeUs = 154500 +sample: + trackIndex = 0 + dataHashCode = -813560096 + isKeyFrame = true + presentationTimeUs = 155250 +sample: + trackIndex = 0 + dataHashCode = 1311045251 + isKeyFrame = true + presentationTimeUs = 156000 +sample: + trackIndex = 0 + dataHashCode = 1388107407 + isKeyFrame = true + presentationTimeUs = 156750 +sample: + trackIndex = 0 + dataHashCode = 1113099440 + isKeyFrame = true + presentationTimeUs = 157500 +sample: + trackIndex = 0 + dataHashCode = -339743582 + isKeyFrame = true + presentationTimeUs = 158250 +sample: + trackIndex = 0 + dataHashCode = -1055895345 + isKeyFrame = true + presentationTimeUs = 159000 +sample: + trackIndex = 0 + dataHashCode = 1869841923 + isKeyFrame = true + presentationTimeUs = 159750 +sample: + trackIndex = 0 + dataHashCode = 229443301 + isKeyFrame = true + presentationTimeUs = 160500 +sample: + trackIndex = 0 + dataHashCode = 1526951012 + isKeyFrame = true + presentationTimeUs = 161250 +sample: + trackIndex = 0 + dataHashCode = -1517436626 + isKeyFrame = true + presentationTimeUs = 162000 +sample: + trackIndex = 0 + dataHashCode = -1403405700 + isKeyFrame = true + presentationTimeUs = 162750 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mkv/sample.mkv.dump b/testdata/src/test/assets/transformerdumps/mkv/sample.mkv.dump new file mode 100644 index 0000000000..00d39b034e --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mkv/sample.mkv.dump @@ -0,0 +1,163 @@ +containerMimeType = video/mp4 +format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B +sample: + trackIndex = 0 + dataHashCode = -252482306 + isKeyFrame = true + presentationTimeUs = 67000 +sample: + trackIndex = 0 + dataHashCode = 67864034 + isKeyFrame = false + presentationTimeUs = 134000 +sample: + trackIndex = 0 + dataHashCode = 897273234 + isKeyFrame = false + presentationTimeUs = 100000 +sample: + trackIndex = 0 + dataHashCode = -1549870586 + isKeyFrame = false + presentationTimeUs = 267000 +sample: + trackIndex = 0 + dataHashCode = 672384813 + isKeyFrame = false + presentationTimeUs = 200000 +sample: + trackIndex = 0 + dataHashCode = -988996493 + isKeyFrame = false + presentationTimeUs = 167000 +sample: + trackIndex = 0 + dataHashCode = 1711151377 + isKeyFrame = false + presentationTimeUs = 234000 +sample: + trackIndex = 0 + dataHashCode = -506806036 + isKeyFrame = false + presentationTimeUs = 400000 +sample: + trackIndex = 0 + dataHashCode = 1902167649 + isKeyFrame = false + presentationTimeUs = 334000 +sample: + trackIndex = 0 + dataHashCode = 2054873212 + isKeyFrame = false + presentationTimeUs = 300000 +sample: + trackIndex = 0 + dataHashCode = 1556608231 + isKeyFrame = false + presentationTimeUs = 367000 +sample: + trackIndex = 0 + dataHashCode = -1648978019 + isKeyFrame = false + presentationTimeUs = 500000 +sample: + trackIndex = 0 + dataHashCode = -484808327 + isKeyFrame = false + presentationTimeUs = 467000 +sample: + trackIndex = 0 + dataHashCode = -20706048 + isKeyFrame = false + presentationTimeUs = 434000 +sample: + trackIndex = 0 + dataHashCode = 2085064574 + isKeyFrame = false + presentationTimeUs = 634000 +sample: + trackIndex = 0 + dataHashCode = -637074022 + isKeyFrame = false + presentationTimeUs = 567000 +sample: + trackIndex = 0 + dataHashCode = -1824027029 + isKeyFrame = false + presentationTimeUs = 534000 +sample: + trackIndex = 0 + dataHashCode = -1701945306 + isKeyFrame = false + presentationTimeUs = 600000 +sample: + trackIndex = 0 + dataHashCode = -952425536 + isKeyFrame = false + presentationTimeUs = 767000 +sample: + trackIndex = 0 + dataHashCode = -1978031576 + isKeyFrame = false + presentationTimeUs = 700000 +sample: + trackIndex = 0 + dataHashCode = -2128215508 + isKeyFrame = false + presentationTimeUs = 667000 +sample: + trackIndex = 0 + dataHashCode = -259850011 + isKeyFrame = false + presentationTimeUs = 734000 +sample: + trackIndex = 0 + dataHashCode = 1920983928 + isKeyFrame = false + presentationTimeUs = 900000 +sample: + trackIndex = 0 + dataHashCode = 1100642337 + isKeyFrame = false + presentationTimeUs = 834000 +sample: + trackIndex = 0 + dataHashCode = 1544917830 + isKeyFrame = false + presentationTimeUs = 800000 +sample: + trackIndex = 0 + dataHashCode = -116205995 + isKeyFrame = false + presentationTimeUs = 867000 +sample: + trackIndex = 0 + dataHashCode = 696343585 + isKeyFrame = false + presentationTimeUs = 1034000 +sample: + trackIndex = 0 + dataHashCode = -644371190 + isKeyFrame = false + presentationTimeUs = 967000 +sample: + trackIndex = 0 + dataHashCode = -1606273467 + isKeyFrame = false + presentationTimeUs = 934000 +sample: + trackIndex = 0 + dataHashCode = -571265861 + isKeyFrame = false + presentationTimeUs = 1000000 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump b/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump new file mode 100644 index 0000000000..05a19cd924 --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mkv/sample_with_srt.mkv.dump @@ -0,0 +1,163 @@ +containerMimeType = video/mp4 +format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.640034 + width = 1080 + height = 720 + selectionFlags = 1 + language = und + initializationData: + data = length 30, hash F6F3D010 + data = length 10, hash 7A0D0F2B +sample: + trackIndex = 0 + dataHashCode = -252482306 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = 67864034 + isKeyFrame = false + presentationTimeUs = 67000 +sample: + trackIndex = 0 + dataHashCode = 897273234 + isKeyFrame = false + presentationTimeUs = 33000 +sample: + trackIndex = 0 + dataHashCode = -1549870586 + isKeyFrame = false + presentationTimeUs = 200000 +sample: + trackIndex = 0 + dataHashCode = 672384813 + isKeyFrame = false + presentationTimeUs = 133000 +sample: + trackIndex = 0 + dataHashCode = -988996493 + isKeyFrame = false + presentationTimeUs = 100000 +sample: + trackIndex = 0 + dataHashCode = 1711151377 + isKeyFrame = false + presentationTimeUs = 167000 +sample: + trackIndex = 0 + dataHashCode = -506806036 + isKeyFrame = false + presentationTimeUs = 333000 +sample: + trackIndex = 0 + dataHashCode = 1902167649 + isKeyFrame = false + presentationTimeUs = 267000 +sample: + trackIndex = 0 + dataHashCode = 2054873212 + isKeyFrame = false + presentationTimeUs = 233000 +sample: + trackIndex = 0 + dataHashCode = 1556608231 + isKeyFrame = false + presentationTimeUs = 300000 +sample: + trackIndex = 0 + dataHashCode = -1648978019 + isKeyFrame = false + presentationTimeUs = 433000 +sample: + trackIndex = 0 + dataHashCode = -484808327 + isKeyFrame = false + presentationTimeUs = 400000 +sample: + trackIndex = 0 + dataHashCode = -20706048 + isKeyFrame = false + presentationTimeUs = 367000 +sample: + trackIndex = 0 + dataHashCode = 2085064574 + isKeyFrame = false + presentationTimeUs = 567000 +sample: + trackIndex = 0 + dataHashCode = -637074022 + isKeyFrame = false + presentationTimeUs = 500000 +sample: + trackIndex = 0 + dataHashCode = -1824027029 + isKeyFrame = false + presentationTimeUs = 467000 +sample: + trackIndex = 0 + dataHashCode = -1701945306 + isKeyFrame = false + presentationTimeUs = 533000 +sample: + trackIndex = 0 + dataHashCode = -952425536 + isKeyFrame = false + presentationTimeUs = 700000 +sample: + trackIndex = 0 + dataHashCode = -1978031576 + isKeyFrame = false + presentationTimeUs = 633000 +sample: + trackIndex = 0 + dataHashCode = -2128215508 + isKeyFrame = false + presentationTimeUs = 600000 +sample: + trackIndex = 0 + dataHashCode = -259850011 + isKeyFrame = false + presentationTimeUs = 667000 +sample: + trackIndex = 0 + dataHashCode = 1920983928 + isKeyFrame = false + presentationTimeUs = 833000 +sample: + trackIndex = 0 + dataHashCode = 1100642337 + isKeyFrame = false + presentationTimeUs = 767000 +sample: + trackIndex = 0 + dataHashCode = 1544917830 + isKeyFrame = false + presentationTimeUs = 733000 +sample: + trackIndex = 0 + dataHashCode = -116205995 + isKeyFrame = false + presentationTimeUs = 800000 +sample: + trackIndex = 0 + dataHashCode = 696343585 + isKeyFrame = false + presentationTimeUs = 967000 +sample: + trackIndex = 0 + dataHashCode = -644371190 + isKeyFrame = false + presentationTimeUs = 900000 +sample: + trackIndex = 0 + dataHashCode = -1606273467 + isKeyFrame = false + presentationTimeUs = 867000 +sample: + trackIndex = 0 + dataHashCode = -571265861 + isKeyFrame = false + presentationTimeUs = 933000 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump new file mode 100644 index 0000000000..3d74318819 --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -0,0 +1,393 @@ +containerMimeType = video/mp4 +format 0: + sampleMimeType = audio/mp4a-latm + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 +format 1: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B +sample: + trackIndex = 1 + dataHashCode = -770308242 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 1 + dataHashCode = -732087136 + isKeyFrame = false + presentationTimeUs = 66733 +sample: + trackIndex = 1 + dataHashCode = 468156717 + isKeyFrame = false + presentationTimeUs = 33366 +sample: + trackIndex = 1 + dataHashCode = 1150349584 + isKeyFrame = false + presentationTimeUs = 200200 +sample: + trackIndex = 1 + dataHashCode = 1443582006 + isKeyFrame = false + presentationTimeUs = 133466 +sample: + trackIndex = 1 + dataHashCode = -310585145 + isKeyFrame = false + presentationTimeUs = 100100 +sample: + trackIndex = 1 + dataHashCode = 807460688 + isKeyFrame = false + presentationTimeUs = 166833 +sample: + trackIndex = 1 + dataHashCode = 1936487090 + isKeyFrame = false + presentationTimeUs = 333666 +sample: + trackIndex = 1 + dataHashCode = -32297181 + isKeyFrame = false + presentationTimeUs = 266933 +sample: + trackIndex = 1 + dataHashCode = 1529616406 + isKeyFrame = false + presentationTimeUs = 233566 +sample: + trackIndex = 1 + dataHashCode = 1949198785 + isKeyFrame = false + presentationTimeUs = 300300 +sample: + trackIndex = 1 + dataHashCode = -147880287 + isKeyFrame = false + presentationTimeUs = 433766 +sample: + trackIndex = 1 + dataHashCode = 1369083472 + isKeyFrame = false + presentationTimeUs = 400400 +sample: + trackIndex = 1 + dataHashCode = 965782073 + isKeyFrame = false + presentationTimeUs = 367033 +sample: + trackIndex = 1 + dataHashCode = -261176150 + isKeyFrame = false + presentationTimeUs = 567233 +sample: + trackIndex = 0 + dataHashCode = 1205768497 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = 837571078 + isKeyFrame = true + presentationTimeUs = 249 +sample: + trackIndex = 0 + dataHashCode = -1991633045 + isKeyFrame = true + presentationTimeUs = 317 +sample: + trackIndex = 0 + dataHashCode = -822987359 + isKeyFrame = true + presentationTimeUs = 1995 +sample: + trackIndex = 0 + dataHashCode = -1141508176 + isKeyFrame = true + presentationTimeUs = 4126 +sample: + trackIndex = 0 + dataHashCode = -226971245 + isKeyFrame = true + presentationTimeUs = 6438 +sample: + trackIndex = 0 + dataHashCode = -2099636855 + isKeyFrame = true + presentationTimeUs = 8818 +sample: + trackIndex = 0 + dataHashCode = 1541550559 + isKeyFrame = true + presentationTimeUs = 11198 +sample: + trackIndex = 0 + dataHashCode = 411148001 + isKeyFrame = true + presentationTimeUs = 13533 +sample: + trackIndex = 0 + dataHashCode = -897603973 + isKeyFrame = true + presentationTimeUs = 16072 +sample: + trackIndex = 0 + dataHashCode = 1478106136 + isKeyFrame = true + presentationTimeUs = 18498 +sample: + trackIndex = 0 + dataHashCode = -1380417145 + isKeyFrame = true + presentationTimeUs = 20878 +sample: + trackIndex = 0 + dataHashCode = 780903644 + isKeyFrame = true + presentationTimeUs = 23326 +sample: + trackIndex = 0 + dataHashCode = 586204432 + isKeyFrame = true + presentationTimeUs = 25911 +sample: + trackIndex = 0 + dataHashCode = -2038771492 + isKeyFrame = true + presentationTimeUs = 28541 +sample: + trackIndex = 0 + dataHashCode = -2065161304 + isKeyFrame = true + presentationTimeUs = 31194 +sample: + trackIndex = 0 + dataHashCode = 468662933 + isKeyFrame = true + presentationTimeUs = 33801 +sample: + trackIndex = 0 + dataHashCode = -358398546 + isKeyFrame = true + presentationTimeUs = 36363 +sample: + trackIndex = 0 + dataHashCode = 1767325983 + isKeyFrame = true + presentationTimeUs = 38811 +sample: + trackIndex = 0 + dataHashCode = 1093095458 + isKeyFrame = true + presentationTimeUs = 41396 +sample: + trackIndex = 0 + dataHashCode = 1687543702 + isKeyFrame = true + presentationTimeUs = 43867 +sample: + trackIndex = 0 + dataHashCode = 1675188486 + isKeyFrame = true + presentationTimeUs = 46588 +sample: + trackIndex = 0 + dataHashCode = 888567545 + isKeyFrame = true + presentationTimeUs = 49173 +sample: + trackIndex = 0 + dataHashCode = -439631803 + isKeyFrame = true + presentationTimeUs = 51871 +sample: + trackIndex = 0 + dataHashCode = 1606694497 + isKeyFrame = true + presentationTimeUs = 54524 +sample: + trackIndex = 0 + dataHashCode = 1747388653 + isKeyFrame = true + presentationTimeUs = 57131 +sample: + trackIndex = 0 + dataHashCode = -734560004 + isKeyFrame = true + presentationTimeUs = 59579 +sample: + trackIndex = 0 + dataHashCode = -975079040 + isKeyFrame = true + presentationTimeUs = 62277 +sample: + trackIndex = 0 + dataHashCode = -1403504710 + isKeyFrame = true + presentationTimeUs = 65020 +sample: + trackIndex = 0 + dataHashCode = 379512981 + isKeyFrame = true + presentationTimeUs = 67627 +sample: + trackIndex = 1 + dataHashCode = -1830836678 + isKeyFrame = false + presentationTimeUs = 500500 +sample: + trackIndex = 1 + dataHashCode = 1767407540 + isKeyFrame = false + presentationTimeUs = 467133 +sample: + trackIndex = 1 + dataHashCode = 918440283 + isKeyFrame = false + presentationTimeUs = 533866 +sample: + trackIndex = 1 + dataHashCode = -1408463661 + isKeyFrame = false + presentationTimeUs = 700700 +sample: + trackIndex = 0 + dataHashCode = -997198863 + isKeyFrame = true + presentationTimeUs = 70234 +sample: + trackIndex = 0 + dataHashCode = 1394492825 + isKeyFrame = true + presentationTimeUs = 72932 +sample: + trackIndex = 0 + dataHashCode = -885232755 + isKeyFrame = true + presentationTimeUs = 75471 +sample: + trackIndex = 0 + dataHashCode = 260871367 + isKeyFrame = true + presentationTimeUs = 78101 +sample: + trackIndex = 0 + dataHashCode = -1505318960 + isKeyFrame = true + presentationTimeUs = 80844 +sample: + trackIndex = 0 + dataHashCode = -390625371 + isKeyFrame = true + presentationTimeUs = 83474 +sample: + trackIndex = 0 + dataHashCode = 1067950751 + isKeyFrame = true + presentationTimeUs = 86149 +sample: + trackIndex = 0 + dataHashCode = -1179436278 + isKeyFrame = true + presentationTimeUs = 88734 +sample: + trackIndex = 0 + dataHashCode = 1906607774 + isKeyFrame = true + presentationTimeUs = 91387 +sample: + trackIndex = 0 + dataHashCode = -800475828 + isKeyFrame = true + presentationTimeUs = 94380 +sample: + trackIndex = 0 + dataHashCode = 1718972977 + isKeyFrame = true + presentationTimeUs = 97282 +sample: + trackIndex = 0 + dataHashCode = -1120448741 + isKeyFrame = true + presentationTimeUs = 99844 +sample: + trackIndex = 0 + dataHashCode = -1718323210 + isKeyFrame = true + presentationTimeUs = 102406 +sample: + trackIndex = 0 + dataHashCode = -422416 + isKeyFrame = true + presentationTimeUs = 105059 +sample: + trackIndex = 0 + dataHashCode = 833757830 + isKeyFrame = true + presentationTimeUs = 107644 +sample: + trackIndex = 1 + dataHashCode = 1569455924 + isKeyFrame = false + presentationTimeUs = 633966 +sample: + trackIndex = 1 + dataHashCode = -1723778407 + isKeyFrame = false + presentationTimeUs = 600600 +sample: + trackIndex = 1 + dataHashCode = 1578275472 + isKeyFrame = false + presentationTimeUs = 667333 +sample: + trackIndex = 1 + dataHashCode = 1989768395 + isKeyFrame = false + presentationTimeUs = 834166 +sample: + trackIndex = 1 + dataHashCode = -1215674502 + isKeyFrame = false + presentationTimeUs = 767433 +sample: + trackIndex = 1 + dataHashCode = -814473606 + isKeyFrame = false + presentationTimeUs = 734066 +sample: + trackIndex = 1 + dataHashCode = 498370894 + isKeyFrame = false + presentationTimeUs = 800800 +sample: + trackIndex = 1 + dataHashCode = -1051506468 + isKeyFrame = false + presentationTimeUs = 967633 +sample: + trackIndex = 1 + dataHashCode = -1025604144 + isKeyFrame = false + presentationTimeUs = 900900 +sample: + trackIndex = 1 + dataHashCode = -913586520 + isKeyFrame = false + presentationTimeUs = 867533 +sample: + trackIndex = 1 + dataHashCode = 1340459242 + isKeyFrame = false + presentationTimeUs = 934266 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.noaudio.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.noaudio.dump new file mode 100644 index 0000000000..d4484cbfb4 --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.noaudio.dump @@ -0,0 +1,163 @@ +containerMimeType = video/mp4 +format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + maxInputSize = 36722 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B +sample: + trackIndex = 0 + dataHashCode = -770308242 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = -732087136 + isKeyFrame = false + presentationTimeUs = 66733 +sample: + trackIndex = 0 + dataHashCode = 468156717 + isKeyFrame = false + presentationTimeUs = 33366 +sample: + trackIndex = 0 + dataHashCode = 1150349584 + isKeyFrame = false + presentationTimeUs = 200200 +sample: + trackIndex = 0 + dataHashCode = 1443582006 + isKeyFrame = false + presentationTimeUs = 133466 +sample: + trackIndex = 0 + dataHashCode = -310585145 + isKeyFrame = false + presentationTimeUs = 100100 +sample: + trackIndex = 0 + dataHashCode = 807460688 + isKeyFrame = false + presentationTimeUs = 166833 +sample: + trackIndex = 0 + dataHashCode = 1936487090 + isKeyFrame = false + presentationTimeUs = 333666 +sample: + trackIndex = 0 + dataHashCode = -32297181 + isKeyFrame = false + presentationTimeUs = 266933 +sample: + trackIndex = 0 + dataHashCode = 1529616406 + isKeyFrame = false + presentationTimeUs = 233566 +sample: + trackIndex = 0 + dataHashCode = 1949198785 + isKeyFrame = false + presentationTimeUs = 300300 +sample: + trackIndex = 0 + dataHashCode = -147880287 + isKeyFrame = false + presentationTimeUs = 433766 +sample: + trackIndex = 0 + dataHashCode = 1369083472 + isKeyFrame = false + presentationTimeUs = 400400 +sample: + trackIndex = 0 + dataHashCode = 965782073 + isKeyFrame = false + presentationTimeUs = 367033 +sample: + trackIndex = 0 + dataHashCode = -261176150 + isKeyFrame = false + presentationTimeUs = 567233 +sample: + trackIndex = 0 + dataHashCode = -1830836678 + isKeyFrame = false + presentationTimeUs = 500500 +sample: + trackIndex = 0 + dataHashCode = 1767407540 + isKeyFrame = false + presentationTimeUs = 467133 +sample: + trackIndex = 0 + dataHashCode = 918440283 + isKeyFrame = false + presentationTimeUs = 533866 +sample: + trackIndex = 0 + dataHashCode = -1408463661 + isKeyFrame = false + presentationTimeUs = 700700 +sample: + trackIndex = 0 + dataHashCode = 1569455924 + isKeyFrame = false + presentationTimeUs = 633966 +sample: + trackIndex = 0 + dataHashCode = -1723778407 + isKeyFrame = false + presentationTimeUs = 600600 +sample: + trackIndex = 0 + dataHashCode = 1578275472 + isKeyFrame = false + presentationTimeUs = 667333 +sample: + trackIndex = 0 + dataHashCode = 1989768395 + isKeyFrame = false + presentationTimeUs = 834166 +sample: + trackIndex = 0 + dataHashCode = -1215674502 + isKeyFrame = false + presentationTimeUs = 767433 +sample: + trackIndex = 0 + dataHashCode = -814473606 + isKeyFrame = false + presentationTimeUs = 734066 +sample: + trackIndex = 0 + dataHashCode = 498370894 + isKeyFrame = false + presentationTimeUs = 800800 +sample: + trackIndex = 0 + dataHashCode = -1051506468 + isKeyFrame = false + presentationTimeUs = 967633 +sample: + trackIndex = 0 + dataHashCode = -1025604144 + isKeyFrame = false + presentationTimeUs = 900900 +sample: + trackIndex = 0 + dataHashCode = -913586520 + isKeyFrame = false + presentationTimeUs = 867533 +sample: + trackIndex = 0 + dataHashCode = 1340459242 + isKeyFrame = false + presentationTimeUs = 934266 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump new file mode 100644 index 0000000000..2e520ebb02 --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump @@ -0,0 +1,232 @@ +containerMimeType = video/mp4 +format 0: + sampleMimeType = audio/mp4a-latm + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 +sample: + trackIndex = 0 + dataHashCode = 1205768497 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = 837571078 + isKeyFrame = true + presentationTimeUs = 249 +sample: + trackIndex = 0 + dataHashCode = -1991633045 + isKeyFrame = true + presentationTimeUs = 317 +sample: + trackIndex = 0 + dataHashCode = -822987359 + isKeyFrame = true + presentationTimeUs = 1995 +sample: + trackIndex = 0 + dataHashCode = -1141508176 + isKeyFrame = true + presentationTimeUs = 4126 +sample: + trackIndex = 0 + dataHashCode = -226971245 + isKeyFrame = true + presentationTimeUs = 6438 +sample: + trackIndex = 0 + dataHashCode = -2099636855 + isKeyFrame = true + presentationTimeUs = 8818 +sample: + trackIndex = 0 + dataHashCode = 1541550559 + isKeyFrame = true + presentationTimeUs = 11198 +sample: + trackIndex = 0 + dataHashCode = 411148001 + isKeyFrame = true + presentationTimeUs = 13533 +sample: + trackIndex = 0 + dataHashCode = -897603973 + isKeyFrame = true + presentationTimeUs = 16072 +sample: + trackIndex = 0 + dataHashCode = 1478106136 + isKeyFrame = true + presentationTimeUs = 18498 +sample: + trackIndex = 0 + dataHashCode = -1380417145 + isKeyFrame = true + presentationTimeUs = 20878 +sample: + trackIndex = 0 + dataHashCode = 780903644 + isKeyFrame = true + presentationTimeUs = 23326 +sample: + trackIndex = 0 + dataHashCode = 586204432 + isKeyFrame = true + presentationTimeUs = 25911 +sample: + trackIndex = 0 + dataHashCode = -2038771492 + isKeyFrame = true + presentationTimeUs = 28541 +sample: + trackIndex = 0 + dataHashCode = -2065161304 + isKeyFrame = true + presentationTimeUs = 31194 +sample: + trackIndex = 0 + dataHashCode = 468662933 + isKeyFrame = true + presentationTimeUs = 33801 +sample: + trackIndex = 0 + dataHashCode = -358398546 + isKeyFrame = true + presentationTimeUs = 36363 +sample: + trackIndex = 0 + dataHashCode = 1767325983 + isKeyFrame = true + presentationTimeUs = 38811 +sample: + trackIndex = 0 + dataHashCode = 1093095458 + isKeyFrame = true + presentationTimeUs = 41396 +sample: + trackIndex = 0 + dataHashCode = 1687543702 + isKeyFrame = true + presentationTimeUs = 43867 +sample: + trackIndex = 0 + dataHashCode = 1675188486 + isKeyFrame = true + presentationTimeUs = 46588 +sample: + trackIndex = 0 + dataHashCode = 888567545 + isKeyFrame = true + presentationTimeUs = 49173 +sample: + trackIndex = 0 + dataHashCode = -439631803 + isKeyFrame = true + presentationTimeUs = 51871 +sample: + trackIndex = 0 + dataHashCode = 1606694497 + isKeyFrame = true + presentationTimeUs = 54524 +sample: + trackIndex = 0 + dataHashCode = 1747388653 + isKeyFrame = true + presentationTimeUs = 57131 +sample: + trackIndex = 0 + dataHashCode = -734560004 + isKeyFrame = true + presentationTimeUs = 59579 +sample: + trackIndex = 0 + dataHashCode = -975079040 + isKeyFrame = true + presentationTimeUs = 62277 +sample: + trackIndex = 0 + dataHashCode = -1403504710 + isKeyFrame = true + presentationTimeUs = 65020 +sample: + trackIndex = 0 + dataHashCode = 379512981 + isKeyFrame = true + presentationTimeUs = 67627 +sample: + trackIndex = 0 + dataHashCode = -997198863 + isKeyFrame = true + presentationTimeUs = 70234 +sample: + trackIndex = 0 + dataHashCode = 1394492825 + isKeyFrame = true + presentationTimeUs = 72932 +sample: + trackIndex = 0 + dataHashCode = -885232755 + isKeyFrame = true + presentationTimeUs = 75471 +sample: + trackIndex = 0 + dataHashCode = 260871367 + isKeyFrame = true + presentationTimeUs = 78101 +sample: + trackIndex = 0 + dataHashCode = -1505318960 + isKeyFrame = true + presentationTimeUs = 80844 +sample: + trackIndex = 0 + dataHashCode = -390625371 + isKeyFrame = true + presentationTimeUs = 83474 +sample: + trackIndex = 0 + dataHashCode = 1067950751 + isKeyFrame = true + presentationTimeUs = 86149 +sample: + trackIndex = 0 + dataHashCode = -1179436278 + isKeyFrame = true + presentationTimeUs = 88734 +sample: + trackIndex = 0 + dataHashCode = 1906607774 + isKeyFrame = true + presentationTimeUs = 91387 +sample: + trackIndex = 0 + dataHashCode = -800475828 + isKeyFrame = true + presentationTimeUs = 94380 +sample: + trackIndex = 0 + dataHashCode = 1718972977 + isKeyFrame = true + presentationTimeUs = 97282 +sample: + trackIndex = 0 + dataHashCode = -1120448741 + isKeyFrame = true + presentationTimeUs = 99844 +sample: + trackIndex = 0 + dataHashCode = -1718323210 + isKeyFrame = true + presentationTimeUs = 102406 +sample: + trackIndex = 0 + dataHashCode = -422416 + isKeyFrame = true + presentationTimeUs = 105059 +sample: + trackIndex = 0 + dataHashCode = 833757830 + isKeyFrame = true + presentationTimeUs = 107644 +released = true diff --git a/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump new file mode 100644 index 0000000000..672582b703 --- /dev/null +++ b/testdata/src/test/assets/transformerdumps/mp4/sample_sef_slow_motion.mp4.dump @@ -0,0 +1,189 @@ +containerMimeType = video/mp4 +format 0: + sampleMimeType = audio/mp4a-latm + channelCount = 2 + sampleRate = 12000 + pcmEncoding = 2 +format 1: + id = 2 + sampleMimeType = video/avc + codecs = avc1.64000D + maxInputSize = 5476 + width = 320 + height = 240 + frameRate = 29.523811 + metadata = entries=[mdta: key=com.android.capture.fps, smta: captureFrameRate=240.0, svcTemporalLayerCount=4, SlowMotion: segments=[Segment: startTimeMs=88, endTimeMs=879, speedDivisor=2, Segment: startTimeMs=1255, endTimeMs=1970, speedDivisor=8]] + initializationData: + data = length 33, hash D3FB879D + data = length 10, hash 7A0D0F2B +sample: + trackIndex = 1 + dataHashCode = -549003117 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 1 + dataHashCode = 593600631 + isKeyFrame = false + presentationTimeUs = 14000 +sample: + trackIndex = 1 + dataHashCode = -961321612 + isKeyFrame = false + presentationTimeUs = 47333 +sample: + trackIndex = 1 + dataHashCode = -386347143 + isKeyFrame = false + presentationTimeUs = 80667 +sample: + trackIndex = 1 + dataHashCode = -1289764147 + isKeyFrame = false + presentationTimeUs = 114000 +sample: + trackIndex = 1 + dataHashCode = 1337088875 + isKeyFrame = false + presentationTimeUs = 147333 +sample: + trackIndex = 1 + dataHashCode = -322406979 + isKeyFrame = false + presentationTimeUs = 180667 +sample: + trackIndex = 1 + dataHashCode = -1688033783 + isKeyFrame = false + presentationTimeUs = 228042 +sample: + trackIndex = 1 + dataHashCode = -700344608 + isKeyFrame = false + presentationTimeUs = 244708 +sample: + trackIndex = 1 + dataHashCode = -1441653629 + isKeyFrame = false + presentationTimeUs = 334083 +sample: + trackIndex = 1 + dataHashCode = 1201357091 + isKeyFrame = false + presentationTimeUs = 267416 +sample: + trackIndex = 1 + dataHashCode = -668484307 + isKeyFrame = false + presentationTimeUs = 234083 +sample: + trackIndex = 1 + dataHashCode = 653508165 + isKeyFrame = false + presentationTimeUs = 300750 +sample: + trackIndex = 1 + dataHashCode = -816848987 + isKeyFrame = false + presentationTimeUs = 467416 +sample: + trackIndex = 1 + dataHashCode = 1842436292 + isKeyFrame = false + presentationTimeUs = 400750 +sample: + trackIndex = 1 + dataHashCode = -559603233 + isKeyFrame = false + presentationTimeUs = 367416 +sample: + trackIndex = 1 + dataHashCode = -666437886 + isKeyFrame = false + presentationTimeUs = 434083 +sample: + trackIndex = 1 + dataHashCode = 182521759 + isKeyFrame = false + presentationTimeUs = 600750 +sample: + trackIndex = 0 + dataHashCode = -212376212 + isKeyFrame = true + presentationTimeUs = 0 +sample: + trackIndex = 0 + dataHashCode = -833872563 + isKeyFrame = true + presentationTimeUs = 416 +sample: + trackIndex = 0 + dataHashCode = -135901925 + isKeyFrame = true + presentationTimeUs = 36499 +sample: + trackIndex = 0 + dataHashCode = -2124187794 + isKeyFrame = true + presentationTimeUs = 44415 +sample: + trackIndex = 0 + dataHashCode = 1016665126 + isKeyFrame = true + presentationTimeUs = 63081 +sample: + trackIndex = 1 + dataHashCode = 2139021989 + isKeyFrame = false + presentationTimeUs = 534083 +sample: + trackIndex = 1 + dataHashCode = 2013165108 + isKeyFrame = false + presentationTimeUs = 500750 +sample: + trackIndex = 1 + dataHashCode = 405675195 + isKeyFrame = false + presentationTimeUs = 567416 +sample: + trackIndex = 1 + dataHashCode = -1893277090 + isKeyFrame = false + presentationTimeUs = 734083 +sample: + trackIndex = 1 + dataHashCode = -1554795381 + isKeyFrame = false + presentationTimeUs = 667416 +sample: + trackIndex = 1 + dataHashCode = 1197099206 + isKeyFrame = false + presentationTimeUs = 634083 +sample: + trackIndex = 1 + dataHashCode = -674808173 + isKeyFrame = false + presentationTimeUs = 700750 +sample: + trackIndex = 1 + dataHashCode = -775517313 + isKeyFrame = false + presentationTimeUs = 867416 +sample: + trackIndex = 1 + dataHashCode = -2045106113 + isKeyFrame = false + presentationTimeUs = 800750 +sample: + trackIndex = 1 + dataHashCode = 305167697 + isKeyFrame = false + presentationTimeUs = 767416 +sample: + trackIndex = 1 + dataHashCode = 554021920 + isKeyFrame = false + presentationTimeUs = 834083 +released = true diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ca514432f2..003c3eb3ba 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; +import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -603,7 +603,7 @@ public abstract class Action { } else { message.setPosition(positionMs); } - message.setHandler(Util.createHandlerForCurrentOrMainLooper()); + message.setLooper(Util.getCurrentOrMainLooper()); message.setDeleteAfterDelivery(deleteAfterDelivery); message.send(); } @@ -685,19 +685,23 @@ public abstract class Action { @Nullable Surface surface, HandlerWrapper handler, @Nullable ActionNode nextAction) { - Handler testThreadHandler = Util.createHandlerForCurrentOrMainLooper(); // Schedule a message on the playback thread to ensure the player is paused immediately. + Looper applicationLooper = Util.getCurrentOrMainLooper(); player .createMessage( (messageType, payload) -> { // Block playback thread until pause command has been sent from test thread. ConditionVariable blockPlaybackThreadCondition = new ConditionVariable(); - testThreadHandler.post( - () -> { - player.pause(); - blockPlaybackThreadCondition.open(); - }); + player + .getClock() + .createHandler(applicationLooper, /* callback= */ null) + .post( + () -> { + player.pause(); + blockPlaybackThreadCondition.open(); + }); try { + player.getClock().onThreadBlocked(); blockPlaybackThreadCondition.block(); } catch (InterruptedException e) { // Ignore. @@ -712,7 +716,7 @@ public abstract class Action { (messageType, payload) -> nextAction.schedule(player, trackSelector, surface, handler)) .setPosition(windowIndex, positionMs) - .setHandler(testThreadHandler) + .setLooper(applicationLooper) .send(); } player.play(); @@ -1049,7 +1053,7 @@ public abstract class Action { player .createMessage( (type, data) -> nextAction.schedule(player, trackSelector, surface, handler)) - .setHandler(Util.createHandlerForCurrentOrMainLooper()) + .setLooper(Util.getCurrentOrMainLooper()) .send(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java index 2507397ca0..86b9bb39f3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/AutoAdvancingFakeClock.java @@ -15,23 +15,12 @@ */ package com.google.android.exoplayer2.testutil; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.HandlerWrapper; - /** * {@link FakeClock} extension which automatically advances time whenever an empty message is * enqueued at a future time. - * - *

The clock time is advanced to the time of enqueued empty messages. The first Handler sending - * messages at a future time will be allowed to advance time to ensure there is only one primary - * time source at a time. This should usually be the Handler of the internal playback loop. You can - * {@link #resetHandler() reset the handler} so that the next Handler that sends messages at a - * future time becomes the primary time source. */ public final class AutoAdvancingFakeClock extends FakeClock { - @Nullable private HandlerWrapper autoAdvancingHandler; - /** Creates the auto-advancing clock with an initial time of 0. */ public AutoAdvancingFakeClock() { this(/* initialTimeMs= */ 0); @@ -43,25 +32,6 @@ public final class AutoAdvancingFakeClock extends FakeClock { * @param initialTimeMs The initial time of the clock in milliseconds. */ public AutoAdvancingFakeClock(long initialTimeMs) { - super(initialTimeMs); - } - - @Override - protected synchronized boolean addHandlerMessageAtTime( - HandlerWrapper handler, int message, long timeMs) { - boolean result = super.addHandlerMessageAtTime(handler, message, timeMs); - if (autoAdvancingHandler == null || autoAdvancingHandler == handler) { - autoAdvancingHandler = handler; - long currentTimeMs = elapsedRealtime(); - if (currentTimeMs < timeMs) { - advanceTime(timeMs - currentTimeMs); - } - } - return result; - } - - /** Resets the internal handler, so that this clock can later be used with another handler. */ - public void resetHandler() { - autoAdvancingHandler = null; + super(initialTimeMs, /* isAutoAdvancing= */ true); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java index d83387a6b2..15677ce41e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DataSourceContractTest.java @@ -194,6 +194,43 @@ public abstract class DataSourceContractTest { } } + /** + * {@link DataSpec#FLAG_ALLOW_GZIP} should either be ignored by {@link DataSource} + * implementations, or correctly handled (i.e. the data is decompressed before being returned from + * {@link DataSource#read(byte[], int, int)}). + */ + @Test + public void gzipFlagDoesntAffectReturnedData() throws Exception { + ImmutableList resources = getTestResources(); + Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + + for (int i = 0; i < resources.size(); i++) { + additionalFailureInfo.setInfo(getFailureLabel(resources, i)); + TestResource resource = resources.get(i); + DataSource dataSource = createDataSource(); + try { + long length = + dataSource.open( + new DataSpec.Builder() + .setUri(resource.getUri()) + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .build()); + byte[] data = + resource.isEndOfInputExpected() + ? Util.readToEnd(dataSource) + : Util.readExactly(dataSource, resource.getExpectedBytes().length); + + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(resource.getExpectedBytes().length); + } + assertThat(data).isEqualTo(resource.getExpectedBytes()); + } finally { + dataSource.close(); + } + additionalFailureInfo.setInfo(null); + } + } + @Test public void resourceNotFound() throws Exception { DataSource dataSource = createDataSource(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DumpableFormat.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DumpableFormat.java new file mode 100644 index 0000000000..80c211914a --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DumpableFormat.java @@ -0,0 +1,101 @@ +/* + * 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.testutil; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Function; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Wraps a {@link Format} to allow dumping it. */ +public final class DumpableFormat implements Dumper.Dumpable { + private final Format format; + public final int index; + + private static final Format DEFAULT_FORMAT = new Format.Builder().build(); + + public DumpableFormat(Format format, int index) { + this.format = format; + this.index = index; + } + + @Override + public void dump(Dumper dumper) { + dumper.startBlock("format " + index); + addIfNonDefault(dumper, "averageBitrate", format -> format.averageBitrate); + addIfNonDefault(dumper, "peakBitrate", format -> format.peakBitrate); + addIfNonDefault(dumper, "id", format -> format.id); + addIfNonDefault(dumper, "containerMimeType", format -> format.containerMimeType); + addIfNonDefault(dumper, "sampleMimeType", format -> format.sampleMimeType); + addIfNonDefault(dumper, "codecs", format -> format.codecs); + addIfNonDefault(dumper, "maxInputSize", format -> format.maxInputSize); + addIfNonDefault(dumper, "width", format -> format.width); + addIfNonDefault(dumper, "height", format -> format.height); + addIfNonDefault(dumper, "frameRate", format -> format.frameRate); + addIfNonDefault(dumper, "rotationDegrees", format -> format.rotationDegrees); + addIfNonDefault(dumper, "pixelWidthHeightRatio", format -> format.pixelWidthHeightRatio); + addIfNonDefault(dumper, "channelCount", format -> format.channelCount); + addIfNonDefault(dumper, "sampleRate", format -> format.sampleRate); + addIfNonDefault(dumper, "pcmEncoding", format -> format.pcmEncoding); + addIfNonDefault(dumper, "encoderDelay", format -> format.encoderDelay); + addIfNonDefault(dumper, "encoderPadding", format -> format.encoderPadding); + addIfNonDefault(dumper, "subsampleOffsetUs", format -> format.subsampleOffsetUs); + addIfNonDefault(dumper, "selectionFlags", format -> format.selectionFlags); + addIfNonDefault(dumper, "language", format -> format.language); + addIfNonDefault(dumper, "label", format -> format.label); + if (format.drmInitData != null) { + dumper.add("drmInitData", format.drmInitData.hashCode()); + } + addIfNonDefault(dumper, "metadata", format -> format.metadata); + if (!format.initializationData.isEmpty()) { + dumper.startBlock("initializationData"); + for (int i = 0; i < format.initializationData.size(); i++) { + dumper.add("data", format.initializationData.get(i)); + } + dumper.endBlock(); + } + dumper.endBlock(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DumpableFormat that = (DumpableFormat) o; + return index == that.index && format.equals(that.format); + } + + @Override + public int hashCode() { + int result = format.hashCode(); + result = 31 * result + index; + return result; + } + + private void addIfNonDefault( + Dumper dumper, String field, Function getFieldFunction) { + @Nullable Object thisValue = getFieldFunction.apply(format); + @Nullable Object defaultValue = getFieldFunction.apply(DEFAULT_FORMAT); + if (!Util.areEqual(thisValue, defaultValue)) { + dumper.add(field, thisValue); + } + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 1bd35e0353..1d1ee3b765 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -227,7 +227,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { protected DrmSessionManager buildDrmSessionManager() { // Do nothing. Interested subclasses may override. - return DrmSessionManager.getDummyDrmSessionManager(); + return DrmSessionManager.DRM_UNSUPPORTED; } protected DefaultTrackSelector buildTrackSelector(HostActivity host) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index b9ee9c9f8d..8232cba48b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -351,6 +351,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc @Nullable private final Player.EventListener eventListener; @Nullable private final AnalyticsListener analyticsListener; + private final Clock clock; private final HandlerThread playerThread; private final HandlerWrapper handler; private final CountDownLatch endedCountDownLatch; @@ -388,6 +389,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc this.actionSchedule = actionSchedule; this.eventListener = eventListener; this.analyticsListener = analyticsListener; + this.clock = playerBuilder.getClock(); timelines = new ArrayList<>(); timelineChangeReasons = new ArrayList<>(); mediaItems = new ArrayList<>(); @@ -399,8 +401,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); - handler = - playerBuilder.getClock().createHandler(playerThread.getLooper(), /* callback= */ null); + handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null); this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; } @@ -476,6 +477,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc * @throws Exception If any exception occurred during playback, release, or due to a timeout. */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + clock.onThreadBlocked(); if (!endedCountDownLatch.await(timeoutMs, MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); } @@ -498,6 +500,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc */ public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) throws TimeoutException, InterruptedException { + clock.onThreadBlocked(); if (!actionScheduleFinishedCountDownLatch.await(timeoutMs, MILLISECONDS)) { throw new TimeoutException("Test playback timed out waiting for action schedule to finish."); } @@ -619,6 +622,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc playerThread.quit(); } }); + clock.onThreadBlocked(); playerThread.join(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java index 9e9642c1cb..376d683267 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -37,10 +37,9 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { */ public static final class Factory { - private static final Random random = new Random(); - private final long chunkDurationUs; private final double bitratePercentStdDev; + private final Random random; /** * Set up factory for {@link FakeAdaptiveDataSet}s with a chunk duration and the standard @@ -50,10 +49,12 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { * @param bitratePercentStdDev The standard deviation used to generate the chunk sizes centered * around the average bitrate of the {@link Format}s. The standard deviation is given in * percent (of the average size). + * @param random The random number generator used to generate the chunk size variation. */ - public Factory(long chunkDurationUs, double bitratePercentStdDev) { + public Factory(long chunkDurationUs, double bitratePercentStdDev, Random random) { this.chunkDurationUs = chunkDurationUs; this.bitratePercentStdDev = bitratePercentStdDev; + this.random = random; } /** @@ -63,8 +64,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { * @param mediaDurationUs The total duration of the fake data set in microseconds. */ public FakeAdaptiveDataSet createDataSet(TrackGroup trackGroup, long mediaDurationUs) { - return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs, - bitratePercentStdDev, random); + return new FakeAdaptiveDataSet( + trackGroup, mediaDurationUs, chunkDurationUs, bitratePercentStdDev, random); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 6f39dec7cd..8db69b3dc7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -33,7 +33,7 @@ import com.google.android.exoplayer2.source.SequenceableLoader; 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.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; @@ -42,9 +42,9 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Sets; import java.io.IOException; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; /** @@ -63,7 +63,7 @@ public class FakeAdaptiveMediaPeriod private final Allocator allocator; private final long durationUs; @Nullable private final TransferListener transferListener; - private final Set> sampleStreams; + private final List> sampleStreams; @Nullable private Callback callback; private boolean prepared; @@ -82,7 +82,7 @@ public class FakeAdaptiveMediaPeriod this.allocator = allocator; this.durationUs = durationUs; this.transferListener = transferListener; - sampleStreams = Sets.newIdentityHashSet(); + sampleStreams = new ArrayList<>(); sequenceableLoader = new CompositeSequenceableLoader(new SequenceableLoader[0]); fakePreparationLoadTaskId = LoadEventInfo.getNewId(); } @@ -143,7 +143,7 @@ public class FakeAdaptiveMediaPeriod @SuppressWarnings({"unchecked", "rawtypes"}) // Casting sample streams created by this class. @Override public long selectTracks( - @NullableType TrackSelection[] selections, + @NullableType ExoTrackSelection[] selections, boolean[] mayRetainStreamFlags, @NullableType SampleStream[] streams, boolean[] streamResetFlags, @@ -157,7 +157,7 @@ public class FakeAdaptiveMediaPeriod streams[i] = null; } if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; + ExoTrackSelection selection = selections[i]; assertThat(selection.length()).isAtLeast(1); TrackGroup trackGroup = selection.getTrackGroup(); assertThat(trackGroupArray.indexOf(trackGroup)).isNotEqualTo(C.INDEX_UNSET); @@ -175,7 +175,7 @@ public class FakeAdaptiveMediaPeriod /* callback= */ this, allocator, positionUs, - DrmSessionManager.DUMMY, + DrmSessionManager.DRM_UNSUPPORTED, new DrmSessionEventListener.EventDispatcher(), new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), mediaSourceEventDispatcher); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 17759eece1..978bc0a047 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -42,7 +42,7 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { FakeChunkSource.Factory chunkSourceFactory) { super( timeline, - DrmSessionManager.DUMMY, + DrmSessionManager.DRM_UNSUPPORTED, /* trackDataFactory= */ (unusedFormat, unusedMediaPeriodId) -> { throw new RuntimeException("Unused TrackDataFactory"); }, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 3e25a13d9c..cc48c30690 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; -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; @@ -55,7 +55,7 @@ public final class FakeChunkSource implements ChunkSource { } public FakeChunkSource createChunkSource( - TrackSelection trackSelection, + ExoTrackSelection trackSelection, long durationUs, @Nullable TransferListener transferListener) { FakeAdaptiveDataSet dataSet = @@ -70,12 +70,12 @@ public final class FakeChunkSource implements ChunkSource { } - private final TrackSelection trackSelection; + private final ExoTrackSelection trackSelection; private final DataSource dataSource; private final FakeAdaptiveDataSet dataSet; - public FakeChunkSource(TrackSelection trackSelection, DataSource dataSource, - FakeAdaptiveDataSet dataSet) { + public FakeChunkSource( + ExoTrackSelection trackSelection, DataSource dataSource, FakeAdaptiveDataSet dataSet) { this.trackSelection = trackSelection; this.dataSource = dataSource; this.dataSet = dataSet; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 64d6ceb0e2..629729bd23 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -15,44 +15,79 @@ */ package com.google.android.exoplayer2.testutil; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + +import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; -import android.os.Message; import android.os.SystemClock; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; +import com.google.common.collect.ComparisonChain; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Fake {@link Clock} implementation that allows to {@link #advanceTime(long) advance the time} * manually to trigger pending timed messages. * *

All timed messages sent by a {@link #createHandler(Looper, Callback) Handler} created from - * this clock are governed by the clock's time. + * this clock are governed by the clock's time. Messages sent through these handlers are not + * triggered until previous messages on any thread have been handled to ensure deterministic + * execution. Note that this includes messages sent from the main Robolectric test thread, meaning + * that these messages are only triggered if the main test thread is idle, which can be explicitly + * requested by calling {@code ShadowLooper.idleMainLooper()}. * *

The clock also sets the time of the {@link SystemClock} to match the {@link #elapsedRealtime() * clock's time}. */ public class FakeClock implements Clock { - private final List wakeUpTimes; - private final List handlerMessages; + private static long messageIdProvider = 0; + + private final boolean isAutoAdvancing; + + @GuardedBy("this") + private final List handlerMessages; + + @GuardedBy("this") + private final Set busyLoopers; + + @GuardedBy("this") private final long bootTimeMs; @GuardedBy("this") private long timeSinceBootMs; + @GuardedBy("this") + private boolean waitingForMessage; + /** - * Creates a fake clock assuming the system was booted exactly at time {@code 0} (the Unix Epoch) - * and {@code initialTimeMs} milliseconds have passed since system boot. + * Creates a fake clock that doesn't auto-advance and assumes that the system was booted exactly + * at time {@code 0} (the Unix Epoch) and {@code initialTimeMs} milliseconds have passed since + * system boot. * * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. */ public FakeClock(long initialTimeMs) { - this(/* bootTimeMs= */ 0, initialTimeMs); + this(/* bootTimeMs= */ 0, initialTimeMs, /* isAutoAdvancing= */ false); + } + + /** + * Creates a fake clock that assumes that the system was booted exactly at time {@code 0} (the + * Unix Epoch) and {@code initialTimeMs} milliseconds have passed since system boot. + * + * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. + * @param isAutoAdvancing Whether the clock should automatically advance the time to the time of + * next message that is due to be sent. + */ + public FakeClock(long initialTimeMs, boolean isAutoAdvancing) { + this(/* bootTimeMs= */ 0, initialTimeMs, isAutoAdvancing); } /** @@ -61,12 +96,15 @@ public class FakeClock implements Clock { * * @param bootTimeMs The time the system was booted since the Unix Epoch, in milliseconds. * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds. + * @param isAutoAdvancing Whether the clock should automatically advance the time to the time of + * next message that is due to be sent. */ - public FakeClock(long bootTimeMs, long initialTimeMs) { + public FakeClock(long bootTimeMs, long initialTimeMs, boolean isAutoAdvancing) { this.bootTimeMs = bootTimeMs; this.timeSinceBootMs = initialTimeMs; - this.wakeUpTimes = new ArrayList<>(); + this.isAutoAdvancing = isAutoAdvancing; this.handlerMessages = new ArrayList<>(); + this.busyLoopers = new HashSet<>(); SystemClock.setCurrentTimeMillis(initialTimeMs); } @@ -76,19 +114,8 @@ public class FakeClock implements Clock { * @param timeDiffMs The amount of time to add to the timestamp in milliseconds. */ public synchronized void advanceTime(long timeDiffMs) { - timeSinceBootMs += timeDiffMs; - SystemClock.setCurrentTimeMillis(timeSinceBootMs); - for (Long wakeUpTime : wakeUpTimes) { - if (wakeUpTime <= timeSinceBootMs) { - notifyAll(); - break; - } - } - for (int i = handlerMessages.size() - 1; i >= 0; i--) { - if (handlerMessages.get(i).maybeSendToTarget(timeSinceBootMs)) { - handlerMessages.remove(i); - } - } + advanceTimeInternal(timeDiffMs); + maybeTriggerMessage(); } @Override @@ -106,91 +133,189 @@ public class FakeClock implements Clock { return elapsedRealtime(); } - @Override - public synchronized void sleep(long sleepTimeMs) { - if (sleepTimeMs <= 0) { - return; - } - Long wakeUpTimeMs = timeSinceBootMs + sleepTimeMs; - wakeUpTimes.add(wakeUpTimeMs); - while (timeSinceBootMs < wakeUpTimeMs) { - try { - wait(); - } catch (InterruptedException e) { - // Ignore InterruptedException as SystemClock.sleep does too. - } - } - wakeUpTimes.remove(wakeUpTimeMs); - } - @Override public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { return new ClockHandler(looper, callback); } - /** Adds a handler post to list of pending messages. */ - protected synchronized boolean addHandlerMessageAtTime( - HandlerWrapper handler, Runnable runnable, long timeMs) { - if (timeMs <= timeSinceBootMs) { - return handler.post(runnable); + @Override + public synchronized void onThreadBlocked() { + @Nullable Looper currentLooper = Looper.myLooper(); + if (currentLooper == null || !waitingForMessage) { + // This isn't a looper message created by this class, so no need to handle the blocking. + return; } - handlerMessages.add(new HandlerMessageData(timeMs, handler, runnable)); - return true; + busyLoopers.add(checkNotNull(Looper.myLooper())); + waitingForMessage = false; + maybeTriggerMessage(); } - /** Adds an empty handler message to list of pending messages. */ - protected synchronized boolean addHandlerMessageAtTime( - HandlerWrapper handler, int message, long timeMs) { - if (timeMs <= timeSinceBootMs) { - return handler.sendEmptyMessage(message); + /** Adds a message to the list of pending messages. */ + protected synchronized void addPendingHandlerMessage(HandlerMessage message) { + handlerMessages.add(message); + if (!waitingForMessage) { + // This method isn't executed from inside a looper message created by this class. + @Nullable Looper currentLooper = Looper.myLooper(); + if (currentLooper == null) { + // This message is triggered from a non-looper thread, so just execute it directly. + maybeTriggerMessage(); + } else { + // Make sure the current looper message is finished before handling the new message. + waitingForMessage = true; + new Handler(checkNotNull(Looper.myLooper())).post(this::onMessageHandled); + } } - handlerMessages.add(new HandlerMessageData(timeMs, handler, message)); - return true; + } + + private synchronized void removePendingHandlerMessages(ClockHandler handler, int what) { + for (int i = handlerMessages.size() - 1; i >= 0; i--) { + HandlerMessage message = handlerMessages.get(i); + if (message.handler.equals(handler) && message.what == what) { + handlerMessages.remove(i); + } + } + handler.handler.removeMessages(what); + } + + private synchronized void removePendingHandlerMessages( + ClockHandler handler, @Nullable Object token) { + for (int i = handlerMessages.size() - 1; i >= 0; i--) { + HandlerMessage message = handlerMessages.get(i); + if (message.handler.equals(handler) && (token == null || message.obj == token)) { + handlerMessages.remove(i); + } + } + handler.handler.removeCallbacksAndMessages(token); + } + + private synchronized boolean hasPendingMessage(ClockHandler handler, int what) { + for (int i = 0; i < handlerMessages.size(); i++) { + HandlerMessage message = handlerMessages.get(i); + if (message.handler.equals(handler) && message.what == what) { + return true; + } + } + return handler.handler.hasMessages(what); + } + + private synchronized void maybeTriggerMessage() { + if (waitingForMessage) { + return; + } + if (handlerMessages.isEmpty()) { + return; + } + Collections.sort(handlerMessages); + int messageIndex = 0; + HandlerMessage message = handlerMessages.get(messageIndex); + int messageCount = handlerMessages.size(); + while (busyLoopers.contains(message.handler.getLooper()) && messageIndex < messageCount) { + messageIndex++; + if (messageIndex == messageCount) { + return; + } + message = handlerMessages.get(messageIndex); + } + if (message.timeMs > timeSinceBootMs) { + if (isAutoAdvancing) { + advanceTimeInternal(message.timeMs - timeSinceBootMs); + } else { + return; + } + } + handlerMessages.remove(messageIndex); + waitingForMessage = true; + if (message.runnable != null) { + message.handler.handler.post(message.runnable); + } else { + message + .handler + .handler + .obtainMessage(message.what, message.arg1, message.arg2, message.obj) + .sendToTarget(); + } + message.handler.internalHandler.post(this::onMessageHandled); + } + + private synchronized void onMessageHandled() { + busyLoopers.remove(Looper.myLooper()); + waitingForMessage = false; + maybeTriggerMessage(); + } + + private synchronized void advanceTimeInternal(long timeDiffMs) { + timeSinceBootMs += timeDiffMs; + SystemClock.setCurrentTimeMillis(timeSinceBootMs); + } + + private static synchronized long getNextMessageId() { + return messageIdProvider++; } /** Message data saved to send messages or execute runnables at a later time on a Handler. */ - private static final class HandlerMessageData { + protected final class HandlerMessage + implements Comparable, HandlerWrapper.Message { - private final long postTime; - private final HandlerWrapper handler; + private final long messageId; + private final long timeMs; + private final ClockHandler handler; @Nullable private final Runnable runnable; - private final int message; + private final int what; + private final int arg1; + private final int arg2; + @Nullable private final Object obj; - public HandlerMessageData(long postTime, HandlerWrapper handler, Runnable runnable) { - this.postTime = postTime; + public HandlerMessage( + long timeMs, + ClockHandler handler, + int what, + int arg1, + int arg2, + @Nullable Object obj, + @Nullable Runnable runnable) { + this.messageId = getNextMessageId(); + this.timeMs = timeMs; this.handler = handler; this.runnable = runnable; - this.message = 0; + this.what = what; + this.arg1 = arg1; + this.arg2 = arg2; + this.obj = obj; } - public HandlerMessageData(long postTime, HandlerWrapper handler, int message) { - this.postTime = postTime; - this.handler = handler; - this.runnable = null; - this.message = message; + /** Returns the time of the message, in milliseconds since boot. */ + /* package */ long getTimeMs() { + return timeMs; } - /** Sends the message and returns whether the message was sent to its target. */ - public boolean maybeSendToTarget(long currentTimeMs) { - if (postTime <= currentTimeMs) { - if (runnable != null) { - handler.post(runnable); - } else { - handler.sendEmptyMessage(message); - } - return true; - } - return false; + @Override + public void sendToTarget() { + addPendingHandlerMessage(/* message= */ this); + } + + @Override + public HandlerWrapper getTarget() { + return handler; + } + + @Override + public int compareTo(HandlerMessage other) { + return ComparisonChain.start() + .compare(this.timeMs, other.timeMs) + .compare(this.messageId, other.messageId) + .result(); } } /** HandlerWrapper implementation using the enclosing Clock to schedule delayed messages. */ private final class ClockHandler implements HandlerWrapper { - private final android.os.Handler handler; + public final Handler handler; + public final Handler internalHandler; public ClockHandler(Looper looper, @Nullable Callback callback) { - handler = new android.os.Handler(looper, callback); + handler = new Handler(looper, callback); + internalHandler = new Handler(looper); } @Override @@ -198,60 +323,101 @@ public class FakeClock implements Clock { return handler.getLooper(); } + @Override + public boolean hasMessages(int what) { + return hasPendingMessage(/* handler= */ this, what); + } + @Override public Message obtainMessage(int what) { - return handler.obtainMessage(what); + return obtainMessage(what, /* obj= */ null); } @Override public Message obtainMessage(int what, @Nullable Object obj) { - return handler.obtainMessage(what, obj); + return obtainMessage(what, /* arg1= */ 0, /* arg2= */ 0, obj); } @Override public Message obtainMessage(int what, int arg1, int arg2) { - return handler.obtainMessage(what, arg1, arg2); + return obtainMessage(what, arg1, arg2, /* obj= */ null); } @Override public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) { - return handler.obtainMessage(what, arg1, arg2, obj); + return new HandlerMessage( + uptimeMillis(), /* handler= */ this, what, arg1, arg2, obj, /* runnable= */ null); + } + + @Override + public boolean sendMessageAtFrontOfQueue(Message msg) { + HandlerMessage message = (HandlerMessage) msg; + new HandlerMessage( + /* timeMs= */ Long.MIN_VALUE, + /* handler= */ this, + message.what, + message.arg1, + message.arg2, + message.obj, + message.runnable) + .sendToTarget(); + return true; } @Override public boolean sendEmptyMessage(int what) { - return handler.sendEmptyMessage(what); + return sendEmptyMessageAtTime(what, uptimeMillis()); } @Override public boolean sendEmptyMessageDelayed(int what, int delayMs) { - return addHandlerMessageAtTime(this, what, uptimeMillis() + delayMs); + return sendEmptyMessageAtTime(what, uptimeMillis() + delayMs); } @Override public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { - return addHandlerMessageAtTime(this, what, uptimeMs); + new HandlerMessage( + uptimeMs, + /* handler= */ this, + what, + /* arg1= */ 0, + /* arg2= */ 0, + /* obj= */ null, + /* runnable= */ null) + .sendToTarget(); + return true; } @Override public void removeMessages(int what) { - handler.removeMessages(what); + removePendingHandlerMessages(/* handler= */ this, what); } @Override public void removeCallbacksAndMessages(@Nullable Object token) { - handler.removeCallbacksAndMessages(token); + removePendingHandlerMessages(/* handler= */ this, token); } @Override public boolean post(Runnable runnable) { - return handler.post(runnable); + return postDelayed(runnable, /* delayMs= */ 0); } @Override public boolean postDelayed(Runnable runnable, long delayMs) { - return addHandlerMessageAtTime(this, runnable, uptimeMillis() + delayMs); + new HandlerMessage( + uptimeMillis() + delayMs, + /* handler= */ this, + /* what= */ 0, + /* arg1= */ 0, + /* arg2= */ 0, + /* obj= */ null, + runnable) + .sendToTarget(); + return true; } } } + + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java index ca2388de1f..5ad0435885 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java @@ -60,7 +60,7 @@ import java.util.concurrent.atomic.AtomicInteger; @RequiresApi(29) public final class FakeExoMediaDrm implements ExoMediaDrm { - public static final ProvisionRequest DUMMY_PROVISION_REQUEST = + public static final ProvisionRequest FAKE_PROVISION_REQUEST = new ProvisionRequest(TestUtil.createByteArray(7, 8, 9), "bar.test"); /** Key for use with the Map returned from {@link FakeExoMediaDrm#queryKeyStatus(byte[])}. */ @@ -192,7 +192,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { @Override public ProvisionRequest getProvisionRequest() { Assertions.checkState(referenceCount > 0); - return DUMMY_PROVISION_REQUEST; + return FAKE_PROVISION_REQUEST; } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 2b79354575..cfd19cf280 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; -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.DataSpec; import com.google.android.exoplayer2.util.Util; @@ -116,7 +116,7 @@ public class FakeMediaPeriod implements MediaPeriod { allocator, TrackDataFactory.singleSampleWithTimeUs(singleSampleTimeUs), mediaSourceEventDispatcher, - DrmSessionManager.DUMMY, + DrmSessionManager.DRM_UNSUPPORTED, new DrmSessionEventListener.EventDispatcher(), /* deferOnPrepared */ false); } @@ -187,8 +187,8 @@ public class FakeMediaPeriod implements MediaPeriod { } /** - * Sets a discontinuity position to be returned from the next call to - * {@link #readDiscontinuity()}. + * Sets a discontinuity position to be returned from the next call to {@link + * #readDiscontinuity()}. * * @param discontinuityPositionUs The position to be returned, in microseconds. */ @@ -196,9 +196,7 @@ public class FakeMediaPeriod implements MediaPeriod { this.discontinuityPositionUs = discontinuityPositionUs; } - /** - * Allows the fake media period to complete preparation. May be called on any thread. - */ + /** Allows the fake media period to complete preparation. May be called on any thread. */ public synchronized void setPreparationComplete() { deferOnPrepared = false; if (playerHandler != null && prepareCallback != null) { @@ -256,7 +254,7 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long selectTracks( - @NullableType TrackSelection[] selections, + @NullableType ExoTrackSelection[] selections, boolean[] mayRetainStreamFlags, @NullableType SampleStream[] streams, boolean[] streamResetFlags, @@ -270,7 +268,7 @@ public class FakeMediaPeriod implements MediaPeriod { streams[i] = null; } if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; + ExoTrackSelection selection = selections[i]; assertThat(selection.length()).isAtLeast(1); TrackGroup trackGroup = selection.getTrackGroup(); assertThat(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET).isTrue(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 77d543a36e..2e7d15073b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -104,7 +104,7 @@ public class FakeMediaSource extends BaseMediaSource { * can be manually set later using {@link #setNewSourceInfo(Timeline)}. */ public FakeMediaSource(@Nullable Timeline timeline, Format... formats) { - this(timeline, DrmSessionManager.DUMMY, formats); + this(timeline, DrmSessionManager.DRM_UNSUPPORTED, formats); } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index a440dd745d..f01cc8d2ca 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -58,7 +58,7 @@ public final class FakeTimeline extends Timeline { * * @param tag The tag to use in the timeline. */ - public static TimelineWindowDefinition createDummy(Object tag) { + public static TimelineWindowDefinition createPlaceholder(Object tag) { return new TimelineWindowDefinition( /* periodCount= */ 1, /* id= */ tag, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index 4e636d993c..a5a88da03e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.upstream.DataReader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import com.google.common.base.Function; import com.google.common.primitives.Bytes; import java.io.EOFException; import java.io.IOException; @@ -34,7 +33,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** A fake {@link TrackOutput}. */ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { @@ -284,81 +282,4 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { } } - private static final class DumpableFormat implements Dumper.Dumpable { - private final Format format; - public final int index; - - private static final Format DEFAULT_FORMAT = new Format.Builder().build(); - - public DumpableFormat(Format format, int index) { - this.format = format; - this.index = index; - } - - @Override - public void dump(Dumper dumper) { - dumper.startBlock("format " + index); - addIfNonDefault(dumper, "averageBitrate", format -> format.averageBitrate); - addIfNonDefault(dumper, "peakBitrate", format -> format.peakBitrate); - addIfNonDefault(dumper, "id", format -> format.id); - addIfNonDefault(dumper, "containerMimeType", format -> format.containerMimeType); - addIfNonDefault(dumper, "sampleMimeType", format -> format.sampleMimeType); - addIfNonDefault(dumper, "codecs", format -> format.codecs); - addIfNonDefault(dumper, "maxInputSize", format -> format.maxInputSize); - addIfNonDefault(dumper, "width", format -> format.width); - addIfNonDefault(dumper, "height", format -> format.height); - addIfNonDefault(dumper, "frameRate", format -> format.frameRate); - addIfNonDefault(dumper, "rotationDegrees", format -> format.rotationDegrees); - addIfNonDefault(dumper, "pixelWidthHeightRatio", format -> format.pixelWidthHeightRatio); - addIfNonDefault(dumper, "channelCount", format -> format.channelCount); - addIfNonDefault(dumper, "sampleRate", format -> format.sampleRate); - addIfNonDefault(dumper, "pcmEncoding", format -> format.pcmEncoding); - addIfNonDefault(dumper, "encoderDelay", format -> format.encoderDelay); - addIfNonDefault(dumper, "encoderPadding", format -> format.encoderPadding); - addIfNonDefault(dumper, "subsampleOffsetUs", format -> format.subsampleOffsetUs); - addIfNonDefault(dumper, "selectionFlags", format -> format.selectionFlags); - addIfNonDefault(dumper, "language", format -> format.language); - addIfNonDefault(dumper, "label", format -> format.label); - if (format.drmInitData != null) { - dumper.add("drmInitData", format.drmInitData.hashCode()); - } - addIfNonDefault(dumper, "metadata", format -> format.metadata); - if (!format.initializationData.isEmpty()) { - dumper.startBlock("initializationData"); - for (int i = 0; i < format.initializationData.size(); i++) { - dumper.add("data", format.initializationData.get(i)); - } - dumper.endBlock(); - } - dumper.endBlock(); - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DumpableFormat that = (DumpableFormat) o; - return index == that.index && format.equals(that.format); - } - - @Override - public int hashCode() { - int result = format.hashCode(); - result = 31 * result + index; - return result; - } - - private void addIfNonDefault( - Dumper dumper, String field, Function getFieldFunction) { - @Nullable Object thisValue = getFieldFunction.apply(format); - @Nullable Object defaultValue = getFieldFunction.apply(DEFAULT_FORMAT); - if (!Util.areEqual(thisValue, defaultValue)) { - dumper.add(field, thisValue); - } - } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java index be78616e8e..38408efff7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -23,14 +23,14 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import java.util.List; /** - * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number of - * calls to its methods. + * A fake {@link ExoTrackSelection} that only returns 1 fixed track, and allows querying the number + * of calls to its methods. */ -public final class FakeTrackSelection implements TrackSelection { +public final class FakeTrackSelection implements ExoTrackSelection { private final TrackGroup rendererTrackGroup; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java index 15d613563e..3a046b6ab0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java @@ -23,8 +23,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.ArrayList; import java.util.List; @@ -41,8 +41,8 @@ public class FakeTrackSelector extends DefaultTrackSelector { /** * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse {@link - * TrackSelection}s during track selection, when it finds previously-selected track selection - * using the same {@link TrackGroup}. + * ExoTrackSelection}s during track selection, when it finds previously-selected track + * selection using the same {@link TrackGroup}. */ public FakeTrackSelector(boolean mayReuseTrackSelection) { this(new FakeTrackSelectionFactory(mayReuseTrackSelection)); @@ -54,18 +54,18 @@ public class FakeTrackSelector extends DefaultTrackSelector { } @Override - protected TrackSelection.@NullableType Definition[] selectAllTracks( + protected ExoTrackSelection.@NullableType Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, @Capabilities int[][][] rendererFormatSupports, @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) { int rendererCount = mappedTrackInfo.getRendererCount(); - TrackSelection.@NullableType Definition[] definitions = - new TrackSelection.Definition[rendererCount]; + ExoTrackSelection.@NullableType Definition[] definitions = + new ExoTrackSelection.Definition[rendererCount]; for (int i = 0; i < rendererCount; i++) { TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); boolean hasTracks = trackGroupArray.length > 0; - definitions[i] = hasTracks ? new TrackSelection.Definition(trackGroupArray.get(0)) : null; + definitions[i] = hasTracks ? new ExoTrackSelection.Definition(trackGroupArray.get(0)) : null; } return definitions; } @@ -75,7 +75,7 @@ public class FakeTrackSelector extends DefaultTrackSelector { return fakeTrackSelectionFactory.trackSelections; } - private static class FakeTrackSelectionFactory implements TrackSelection.Factory { + private static class FakeTrackSelectionFactory implements ExoTrackSelection.Factory { private final List trackSelections; private final boolean mayReuseTrackSelection; @@ -86,14 +86,14 @@ public class FakeTrackSelector extends DefaultTrackSelector { } @Override - public TrackSelection[] createTrackSelections( - TrackSelection.@NullableType Definition[] definitions, + public ExoTrackSelection[] createTrackSelections( + ExoTrackSelection.@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter, MediaPeriodId mediaPeriodId, Timeline timeline) { - TrackSelection[] selections = new TrackSelection[definitions.length]; + ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length]; for (int i = 0; i < definitions.length; i++) { - TrackSelection.Definition definition = definitions[i]; + ExoTrackSelection.Definition definition = definitions[i]; if (definition != null) { selections[i] = createTrackSelection(definition.group); } @@ -101,7 +101,7 @@ public class FakeTrackSelector extends DefaultTrackSelector { return selections; } - private TrackSelection createTrackSelection(TrackGroup trackGroup) { + private ExoTrackSelection createTrackSelection(TrackGroup trackGroup) { if (mayReuseTrackSelection) { for (FakeTrackSelection trackSelection : trackSelections) { if (trackSelection.getTrackGroup().equals(trackGroup)) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HttpDataSourceTestEnv.java similarity index 57% rename from library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/HttpDataSourceTestEnv.java index 92fd23e0a7..fe15120260 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceContractTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HttpDataSourceTestEnv.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -13,28 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.upstream; + +package com.google.android.exoplayer2.testutil; 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.android.exoplayer2.testutil.WebServerDispatcher; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.common.collect.ImmutableList; -import java.util.function.Function; -import okhttp3.HttpUrl; +import java.io.IOException; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.After; -import org.junit.Before; -import org.junit.runner.RunWith; - -/** {@link DataSource} contract tests for {@link DefaultHttpDataSource}. */ -@RunWith(AndroidJUnit4.class) -public class DefaultHttpDataSourceContractTest extends DataSourceContractTest { +import org.junit.Rule; +import org.junit.rules.ExternalResource; +/** A JUnit {@link Rule} that creates test resources for {@link HttpDataSource} contract tests. */ +public class HttpDataSourceTestEnv extends ExternalResource { private static int seed = 0; private static final WebServerDispatcher.Resource RANGE_SUPPORTED = new WebServerDispatcher.Resource.Builder() @@ -66,14 +60,45 @@ public class DefaultHttpDataSourceContractTest extends DataSourceContractTest { .resolvesToUnknownLength(true) .build(); + private static final WebServerDispatcher.Resource GZIP_ENABLED = + new WebServerDispatcher.Resource.Builder() + .setPath("/gzip/enabled") + .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) + .setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED) + .build(); + + private static final WebServerDispatcher.Resource GZIP_FORCED = + new WebServerDispatcher.Resource.Builder() + .setPath("/gzip/forced") + .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) + .setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED) + .build(); + private static final WebServerDispatcher.Resource REDIRECTS_TO_RANGE_SUPPORTED = RANGE_SUPPORTED.buildUpon().setPath("/redirects/to/range/supported").build(); private final MockWebServer originServer = new MockWebServer(); private final MockWebServer redirectionServer = new MockWebServer(); - @Before - public void startServers() throws Exception { + public ImmutableList getServedResources() { + return ImmutableList.of( + createTestResource("range supported", RANGE_SUPPORTED), + createTestResource("range supported, length unknown", RANGE_SUPPORTED_LENGTH_UNKNOWN), + createTestResource("range not supported", RANGE_NOT_SUPPORTED), + createTestResource( + "range not supported, length unknown", RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN), + createTestResource("gzip enabled", GZIP_ENABLED), + createTestResource("gzip forced", GZIP_FORCED), + createTestResource( + "302 redirect", REDIRECTS_TO_RANGE_SUPPORTED, /* server= */ redirectionServer)); + } + + public String getNonexistentUrl() { + return originServer.url("/not/a/real/path").toString(); + } + + @Override + protected void before() throws Throwable { originServer.start(); originServer.setDispatcher( WebServerDispatcher.forResources( @@ -81,7 +106,9 @@ public class DefaultHttpDataSourceContractTest extends DataSourceContractTest { RANGE_SUPPORTED, RANGE_SUPPORTED_LENGTH_UNKNOWN, RANGE_NOT_SUPPORTED, - RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN))); + RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN, + GZIP_ENABLED, + GZIP_FORCED))); redirectionServer.start(); redirectionServer.setDispatcher( @@ -99,41 +126,26 @@ public class DefaultHttpDataSourceContractTest extends DataSourceContractTest { }); } - @After - public void shutdownServers() throws Exception { - originServer.shutdown(); - redirectionServer.shutdown(); - } - @Override - protected DataSource createDataSource() { - return new DefaultHttpDataSource.Factory().createDataSource(); + protected void after() { + try { + originServer.shutdown(); + redirectionServer.shutdown(); + } catch (IOException e) { + throw new RuntimeException(e); + } } - @Override - protected ImmutableList getTestResources() { - return ImmutableList.of( - toTestResource("range supported", RANGE_SUPPORTED, originServer::url), - toTestResource( - "range supported, length unknown", RANGE_SUPPORTED_LENGTH_UNKNOWN, originServer::url), - toTestResource("range not supported", RANGE_NOT_SUPPORTED, originServer::url), - toTestResource( - "range not supported, length unknown", - RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN, - originServer::url), - toTestResource("302 redirect", REDIRECTS_TO_RANGE_SUPPORTED, redirectionServer::url)); + private DataSourceContractTest.TestResource createTestResource( + String name, WebServerDispatcher.Resource resource) { + return createTestResource(name, resource, originServer); } - @Override - protected Uri getNotFoundUri() { - return Uri.parse(originServer.url("/not/a/real/path").toString()); - } - - private static TestResource toTestResource( - String name, WebServerDispatcher.Resource resource, Function urlResolver) { - return new TestResource.Builder() + private static DataSourceContractTest.TestResource createTestResource( + String name, WebServerDispatcher.Resource resource, MockWebServer server) { + return new DataSourceContractTest.TestResource.Builder() .setName(name) - .setUri(Uri.parse(urlResolver.apply(resource.getPath()).toString())) + .setUri(Uri.parse(server.url(resource.getPath()).toString())) .setExpectedBytes(resource.getData()) .setEndOfInputExpected(!resource.resolvesToUnknownLength()) .build(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index eac0ea0f3a..cbb72a9557 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; 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.util.ConditionVariable; import java.util.ArrayList; import java.util.Arrays; @@ -102,7 +102,7 @@ public final class MediaPeriodAsserts { // - One selection with one track per group, two tracks or all tracks. // - Two selections with tracks from multiple groups, or tracks from a single group. // - Multiple selections with tracks from all groups. - List> testSelections = new ArrayList<>(); + List> testSelections = new ArrayList<>(); for (int i = 0; i < trackGroupArray.length; i++) { TrackGroup trackGroup = trackGroupArray.get(i); for (int j = 0; j < trackGroup.length; j++) { @@ -112,7 +112,7 @@ public final class MediaPeriodAsserts { testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, 0, 1))); testSelections.add( Arrays.asList( - new TrackSelection[] { + new ExoTrackSelection[] { new TestTrackSelection(trackGroup, 0), new TestTrackSelection(trackGroup, 1) })); } @@ -130,7 +130,7 @@ public final class MediaPeriodAsserts { for (int j = i + 1; j < trackGroupArray.length; j++) { testSelections.add( Arrays.asList( - new TrackSelection[] { + new ExoTrackSelection[] { new TestTrackSelection(trackGroupArray.get(i), 0), new TestTrackSelection(trackGroupArray.get(j), 0) })); @@ -138,7 +138,7 @@ public final class MediaPeriodAsserts { } } if (trackGroupArray.length > 2) { - List selectionsFromAllGroups = new ArrayList<>(); + List selectionsFromAllGroups = new ArrayList<>(); for (int i = 0; i < trackGroupArray.length; i++) { selectionsFromAllGroups.add(new TestTrackSelection(trackGroupArray.get(i), 0)); } @@ -147,7 +147,7 @@ public final class MediaPeriodAsserts { // Verify for each case that stream keys can be used to create filtered tracks which still // contain at least all requested formats. - for (List testSelection : testSelections) { + for (List testSelection : testSelections) { List streamKeys = mediaPeriod.getStreamKeys(testSelection); if (streamKeys.isEmpty()) { // Manifests won't be filtered if stream key is empty. @@ -158,7 +158,7 @@ public final class MediaPeriodAsserts { MediaPeriod filteredMediaPeriod = mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0); TrackGroupArray filteredTrackGroupArray = prepareAndGetTrackGroups(filteredMediaPeriod); - for (TrackSelection trackSelection : testSelection) { + for (ExoTrackSelection trackSelection : testSelection) { if (ignoredMimeType != null && ignoredMimeType.equals(trackSelection.getFormat(0).sampleMimeType)) { continue; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 3748f697fe..1eb4450fb6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Clock; import java.util.List; /** @@ -75,6 +76,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public Clock getClock() { + throw new UnsupportedOperationException(); + } + @Override public void addListener(Player.EventListener listener) { throw new UnsupportedOperationException(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index f52f4380cf..11687cd2d2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -89,19 +89,18 @@ public class TestUtil { } /** - * Generates a random string with the specified maximum length. + * Generates a random string with the specified length. * - * @param maximumLength The maximum length of the string. + * @param length The length of the string. * @param random A source of randomness. * @return The generated string. */ - public static String buildTestString(int maximumLength, Random random) { - int length = random.nextInt(maximumLength); - StringBuilder builder = new StringBuilder(length); + public static String buildTestString(int length, Random random) { + char[] chars = new char[length]; for (int i = 0; i < length; i++) { - builder.append((char) random.nextInt()); + chars[i] = (char) random.nextInt(); } - return builder.toString(); + return new String(chars); } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java index 62dcc55f15..0ba0835f6c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/WebServerDispatcher.java @@ -15,14 +15,25 @@ */ package com.google.android.exoplayer2.testutil; +import static com.google.android.exoplayer2.testutil.WebServerDispatcher.Resource.GZIP_SUPPORT_DISABLED; +import static com.google.android.exoplayer2.testutil.WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; import static java.lang.Math.max; import static java.lang.Math.min; import android.util.Pair; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import okhttp3.mockwebserver.Dispatcher; @@ -41,21 +52,60 @@ public class WebServerDispatcher extends Dispatcher { /** A resource served by {@link WebServerDispatcher}. */ public static class Resource { + /** + * The level of gzip support offered by the server for a resource. + * + *

One of: + * + *

    + *
  • {@link #GZIP_SUPPORT_DISABLED} + *
  • {@link #GZIP_SUPPORT_ENABLED} + *
  • {@link #GZIP_SUPPORT_FORCED} + *
+ */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({GZIP_SUPPORT_DISABLED, GZIP_SUPPORT_ENABLED, GZIP_SUPPORT_FORCED}) + private @interface GzipSupport {} + + /** The server doesn't support gzip. */ + public static final int GZIP_SUPPORT_DISABLED = 1; + + /** + * The server supports gzip. Responses are only compressed if the request signals "gzip" is an + * acceptable content-coding using an {@code Accept-Encoding} header. + */ + public static final int GZIP_SUPPORT_ENABLED = 2; + + /** + * The server supports gzip. Responses are compressed if the request contains no {@code + * Accept-Encoding} header or one that accepts {@code "gzip"}. + * + *

RFC 2616 14.3 recommends a server use {@code "identity"} content-coding if no {@code + * Accept-Encoding} is present, but some servers will still compress responses in this case. + * This option mimics that behaviour. + */ + public static final int GZIP_SUPPORT_FORCED = 3; + /** Builder for {@link Resource}. */ public static class Builder { private @MonotonicNonNull String path; private byte @MonotonicNonNull [] data; private boolean supportsRangeRequests; private boolean resolvesToUnknownLength; + @GzipSupport private int gzipSupport; /** Constructs an instance. */ - public Builder() {} + public Builder() { + this.gzipSupport = GZIP_SUPPORT_DISABLED; + } private Builder(Resource resource) { this.path = resource.getPath(); this.data = resource.getData(); this.supportsRangeRequests = resource.supportsRangeRequests(); this.resolvesToUnknownLength = resource.resolvesToUnknownLength(); + this.gzipSupport = resource.getGzipSupport(); } /** @@ -89,7 +139,7 @@ public class WebServerDispatcher extends Dispatcher { } /** - * Sets if the resource should resolve to an unknown length. Defaults to false. + * Sets if the server shouldn't include the resource length in header responses. * *

If true, responses to unbound requests won't include a Content-Length header and * Content-Range headers won't include the total resource length. @@ -101,10 +151,29 @@ public class WebServerDispatcher extends Dispatcher { return this; } + /** + * Sets the level of gzip support for this resource. Defaults to {@link + * #GZIP_SUPPORT_DISABLED}. + * + * @return this builder, for convenience. + */ + public Builder setGzipSupport(@GzipSupport int gzipSupport) { + this.gzipSupport = gzipSupport; + return this; + } + /** Builds the {@link Resource}. */ public Resource build() { + if (gzipSupport != GZIP_SUPPORT_DISABLED) { + checkState(!supportsRangeRequests, "Can't enable compression & range requests."); + checkState(!resolvesToUnknownLength, "Can't enable compression if length isn't known."); + } return new Resource( - checkNotNull(path), checkNotNull(data), supportsRangeRequests, resolvesToUnknownLength); + checkNotNull(path), + checkNotNull(data), + supportsRangeRequests, + resolvesToUnknownLength, + gzipSupport); } } @@ -112,13 +181,19 @@ public class WebServerDispatcher extends Dispatcher { private final byte[] data; private final boolean supportsRangeRequests; private final boolean resolvesToUnknownLength; + @GzipSupport private final int gzipSupport; private Resource( - String path, byte[] data, boolean supportsRangeRequests, boolean resolvesToUnknownLength) { + String path, + byte[] data, + boolean supportsRangeRequests, + boolean resolvesToUnknownLength, + @GzipSupport int gzipSupport) { this.path = path; this.data = data; this.supportsRangeRequests = supportsRangeRequests; this.resolvesToUnknownLength = resolvesToUnknownLength; + this.gzipSupport = gzipSupport; } /** Returns the path this resource is available at. */ @@ -141,12 +216,22 @@ public class WebServerDispatcher extends Dispatcher { return resolvesToUnknownLength; } + /** Returns the level of gzip support the server should provide for this resource. */ + @GzipSupport + public int getGzipSupport() { + return gzipSupport; + } + /** Returns a new {@link Builder} initialized with the values from this instance. */ public Builder buildUpon() { return new Builder(this); } } + /** Matches an Accept-Encoding header value (format defined in RFC 2616 section 14.3). */ + private static final Pattern ACCEPT_ENCODING_PATTERN = + Pattern.compile("\\W*(\\w+|\\*)(?:;q=(\\d+\\.?\\d*))?\\W*"); + private final ImmutableMap resourcesByPath; /** @@ -171,9 +256,39 @@ public class WebServerDispatcher extends Dispatcher { if (resource.supportsRangeRequests()) { response.setHeader("Accept-ranges", "bytes"); } - String rangeHeader = request.getHeader("Range"); + @Nullable ImmutableMap acceptEncodingHeader = getAcceptEncodingHeader(request); + @Nullable String preferredContentCoding; + if (resource.getGzipSupport() == GZIP_SUPPORT_FORCED && acceptEncodingHeader == null) { + preferredContentCoding = "gzip"; + } else { + ImmutableList supportedContentCodings = + resource.getGzipSupport() == GZIP_SUPPORT_DISABLED + ? ImmutableList.of("identity") + : ImmutableList.of("gzip", "identity"); + preferredContentCoding = + getPreferredContentCoding(acceptEncodingHeader, supportedContentCodings); + } + if (preferredContentCoding == null) { + // None of the supported encodings are accepted by the client. + return response.setResponseCode(406); + } + + @Nullable String rangeHeader = request.getHeader("Range"); if (!resource.supportsRangeRequests() || rangeHeader == null) { - response.setBody(new Buffer().write(resourceData)); + switch (preferredContentCoding) { + case "gzip": + response + .setBody(new Buffer().write(Util.gzip(resourceData))) + .setHeader("Content-Encoding", "gzip"); + break; + case "identity": + response + .setBody(new Buffer().write(resourceData)) + .setHeader("Content-Encoding", "identity"); + break; + default: + throw new IllegalStateException("Unexpected content coding: " + preferredContentCoding); + } if (resource.resolvesToUnknownLength()) { response.setHeader("Content-Length", ""); } @@ -181,7 +296,7 @@ public class WebServerDispatcher extends Dispatcher { } @Nullable - Pair<@NullableType Integer, @NullableType Integer> range = parseRangeHeader(rangeHeader); + Pair<@NullableType Integer, @NullableType Integer> range = getRangeHeader(rangeHeader); if (range == null || (range.first != null && range.first >= resourceData.length)) { return response @@ -243,11 +358,83 @@ public class WebServerDispatcher extends Dispatcher { .setBody(new Buffer().write(resourceData, range.first, end - range.first)); } + /** + * Parses an RFC 2616 14.3 Accept-Encoding header into a map from content-coding to qvalue. + * + *

Returns null if the header is not present. + * + *

Missing qvalues are stored in the map as -1. + */ + @Nullable + private static ImmutableMap getAcceptEncodingHeader(RecordedRequest request) { + @Nullable List headers = request.getHeaders().toMultimap().get("Accept-Encoding"); + if (headers == null) { + return null; + } + String header = Joiner.on(",").join(headers); + String[] encodings = Util.split(header, ","); + ImmutableMap.Builder parsedEncodings = ImmutableMap.builder(); + for (String encoding : encodings) { + Matcher matcher = ACCEPT_ENCODING_PATTERN.matcher(encoding); + if (!matcher.matches()) { + continue; + } + String contentCoding = checkNotNull(matcher.group(1)); + @Nullable String qvalue = matcher.group(2); + parsedEncodings.put(contentCoding, qvalue == null ? -1f : Float.parseFloat(qvalue)); + } + return parsedEncodings.build(); + } + + /** + * Returns the preferred content-coding based on the (optional) Accept-Encoding header, or null if + * none of {@code supportedContentCodings} are accepted by the client. + * + *

The selection algorithm is described in RFC 2616 section 14.3. + * + * @param acceptEncodingHeader The Accept-Encoding header parsed into a map from content-coding to + * qvalue (absent qvalues are represented by -1), or null if the header isn't present. + * @param supportedContentCodings A list of content-codings supported by the server in order of + * preference. + */ + @Nullable + private static String getPreferredContentCoding( + @Nullable ImmutableMap acceptEncodingHeader, + List supportedContentCodings) { + if (acceptEncodingHeader == null) { + return "identity"; + } + if (!acceptEncodingHeader.containsKey("identity") && !acceptEncodingHeader.containsKey("*")) { + acceptEncodingHeader = + ImmutableMap.builder() + .putAll(acceptEncodingHeader) + .put("identity", -1f) + .build(); + } + float asteriskQvalue = acceptEncodingHeader.getOrDefault("*", 0f); + @Nullable String preferredContentCoding = null; + float preferredQvalue = Integer.MIN_VALUE; + for (String supportedContentCoding : supportedContentCodings) { + float qvalue = acceptEncodingHeader.getOrDefault(supportedContentCoding, 0f); + if (!acceptEncodingHeader.containsKey(supportedContentCoding) + && asteriskQvalue != 0 + && asteriskQvalue > preferredQvalue) { + preferredContentCoding = supportedContentCoding; + preferredQvalue = asteriskQvalue; + } else if (qvalue != 0 && qvalue > preferredQvalue) { + preferredContentCoding = supportedContentCoding; + preferredQvalue = qvalue; + } + } + + return preferredContentCoding; + } + /** * Parses an RFC 7233 Range header to its component parts. Returns null if the Range is invalid. */ @Nullable - private static Pair<@NullableType Integer, @NullableType Integer> parseRangeHeader( + private static Pair<@NullableType Integer, @NullableType Integer> getRangeHeader( String rangeHeader) { Pattern rangePattern = Pattern.compile("bytes=(\\d*)-(\\d*)"); Matcher rangeMatcher = rangePattern.matcher(rangeHeader); diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java index ba0a022270..28e57d8e66 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -16,23 +16,28 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; -import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.robolectric.Shadows.shadowOf; import android.os.ConditionVariable; +import android.os.Handler; import android.os.HandlerThread; +import android.os.Message; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; -import java.util.concurrent.CountDownLatch; +import com.google.common.base.Objects; +import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link FakeClock}. */ @RunWith(AndroidJUnit4.class) public final class FakeClockTest { - private static final long TIMEOUT_MS = 10_000; - @Test public void currentTimeMillis_withoutBootTime() { FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 10); @@ -41,19 +46,22 @@ public final class FakeClockTest { @Test public void currentTimeMillis_withBootTime() { - FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 150, /* initialTimeMs= */ 200); + FakeClock fakeClock = + new FakeClock( + /* bootTimeMs= */ 150, /* initialTimeMs= */ 200, /* isAutoAdvancing= */ false); assertThat(fakeClock.currentTimeMillis()).isEqualTo(350); } @Test - public void currentTimeMillis_advanceTime_currentTimeHasAdvanced() { - FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50); + public void currentTimeMillis_afterAdvanceTime_currentTimeHasAdvanced() { + FakeClock fakeClock = + new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50, /* isAutoAdvancing= */ false); fakeClock.advanceTime(/* timeDiffMs */ 250); assertThat(fakeClock.currentTimeMillis()).isEqualTo(400); } @Test - public void testAdvanceTime() { + public void elapsedRealtime_afterAdvanceTime_timeHasAdvanced() { FakeClock fakeClock = new FakeClock(2000); assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000); fakeClock.advanceTime(500); @@ -63,49 +71,92 @@ public final class FakeClockTest { } @Test - public void testSleep() throws InterruptedException { - FakeClock fakeClock = new FakeClock(0); - SleeperThread sleeperThread = new SleeperThread(fakeClock, 1000); - sleeperThread.start(); - assertThat(sleeperThread.waitUntilAsleep(TIMEOUT_MS)).isTrue(); - assertThat(sleeperThread.isSleeping()).isTrue(); - fakeClock.advanceTime(1000); - sleeperThread.join(TIMEOUT_MS); - assertThat(sleeperThread.isSleeping()).isFalse(); + public void createHandler_obtainMessageSendToTarget_triggersMessage() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + TestCallback callback = new TestCallback(); + HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); - sleeperThread = new SleeperThread(fakeClock, 0); - sleeperThread.start(); - sleeperThread.join(); - assertThat(sleeperThread.isSleeping()).isFalse(); + Object testObject = new Object(); + handler.obtainMessage(/* what= */ 1).sendToTarget(); + handler.obtainMessage(/* what= */ 2, /* obj= */ testObject).sendToTarget(); + handler.obtainMessage(/* what= */ 3, /* arg1= */ 99, /* arg2= */ 44).sendToTarget(); + handler + .obtainMessage(/* what= */ 4, /* arg1= */ 88, /* arg2= */ 33, /* obj=*/ testObject) + .sendToTarget(); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); - SleeperThread[] sleeperThreads = new SleeperThread[5]; - sleeperThreads[0] = new SleeperThread(fakeClock, 1000); - sleeperThreads[1] = new SleeperThread(fakeClock, 1000); - sleeperThreads[2] = new SleeperThread(fakeClock, 2000); - sleeperThreads[3] = new SleeperThread(fakeClock, 3000); - sleeperThreads[4] = new SleeperThread(fakeClock, 4000); - for (SleeperThread thread : sleeperThreads) { - thread.start(); - assertThat(thread.waitUntilAsleep(TIMEOUT_MS)).isTrue(); - } - assertSleepingStates(new boolean[] {true, true, true, true, true}, sleeperThreads); - fakeClock.advanceTime(1500); - assertThat(sleeperThreads[0].waitUntilAwake(TIMEOUT_MS)).isTrue(); - assertThat(sleeperThreads[1].waitUntilAwake(TIMEOUT_MS)).isTrue(); - assertSleepingStates(new boolean[] {false, false, true, true, true}, sleeperThreads); - fakeClock.advanceTime(2000); - assertThat(sleeperThreads[2].waitUntilAwake(TIMEOUT_MS)).isTrue(); - assertThat(sleeperThreads[3].waitUntilAwake(TIMEOUT_MS)).isTrue(); - assertSleepingStates(new boolean[] {false, false, false, false, true}, sleeperThreads); - fakeClock.advanceTime(2000); - for (SleeperThread thread : sleeperThreads) { - thread.join(TIMEOUT_MS); - } - assertSleepingStates(new boolean[] {false, false, false, false, false}, sleeperThreads); + assertThat(callback.messages) + .containsExactly( + new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), + new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ testObject), + new MessageData(/* what= */ 3, /* arg1= */ 99, /* arg2= */ 44, /* obj=*/ null), + new MessageData(/* what= */ 4, /* arg1= */ 88, /* arg2= */ 33, /* obj=*/ testObject)) + .inOrder(); } @Test - public void testPostDelayed() { + public void createHandler_sendEmptyMessage_triggersMessageAtCorrectTime() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + TestCallback callback = new TestCallback(); + HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); + + handler.sendEmptyMessage(/* what= */ 1); + handler.sendEmptyMessageAtTime(/* what= */ 2, /* uptimeMs= */ fakeClock.uptimeMillis() + 60); + handler.sendEmptyMessageDelayed(/* what= */ 3, /* delayMs= */ 50); + handler.sendEmptyMessage(/* what= */ 4); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); + + assertThat(callback.messages) + .containsExactly( + new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), + new MessageData(/* what= */ 4, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)) + .inOrder(); + + fakeClock.advanceTime(50); + shadowOf(handler.getLooper()).idle(); + + assertThat(callback.messages).hasSize(3); + assertThat(Iterables.getLast(callback.messages)) + .isEqualTo(new MessageData(/* what= */ 3, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); + + fakeClock.advanceTime(50); + shadowOf(handler.getLooper()).idle(); + + assertThat(callback.messages).hasSize(4); + assertThat(Iterables.getLast(callback.messages)) + .isEqualTo(new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); + } + + @Test + public void createHandler_sendMessageAtFrontOfQueue_sendsMessageFirst() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + TestCallback callback = new TestCallback(); + HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); + + handler.obtainMessage(/* what= */ 1).sendToTarget(); + handler.sendMessageAtFrontOfQueue(handler.obtainMessage(/* what= */ 2)); + handler.obtainMessage(/* what= */ 3).sendToTarget(); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); + + assertThat(callback.messages) + .containsExactly( + new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), + new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), + new MessageData(/* what= */ 3, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)) + .inOrder(); + } + + @Test + public void createHandler_postDelayed_triggersMessagesUpToCurrentTime() { HandlerThread handlerThread = new HandlerThread("FakeClockTest"); handlerThread.start(); FakeClock fakeClock = new FakeClock(0); @@ -122,34 +173,202 @@ public final class FakeClockTest { handler.postDelayed(testRunnables[0], 0); handler.postDelayed(testRunnables[1], 100); handler.postDelayed(testRunnables[2], 200); - waitForHandler(handler); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables); fakeClock.advanceTime(150); handler.postDelayed(testRunnables[3], 50); handler.postDelayed(testRunnables[4], 100); - waitForHandler(handler); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables); fakeClock.advanceTime(50); - waitForHandler(handler); + shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, true, true, true, false}, testRunnables); fakeClock.advanceTime(1000); - waitForHandler(handler); + shadowOf(handler.getLooper()).idle(); assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables); } - private static void assertSleepingStates(boolean[] states, SleeperThread[] sleeperThreads) { - for (int i = 0; i < sleeperThreads.length; i++) { - assertThat(sleeperThreads[i].isSleeping()).isEqualTo(states[i]); - } + @Test + public void createHandler_removeMessages_removesMessages() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + TestCallback callback = new TestCallback(); + HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); + TestCallback otherCallback = new TestCallback(); + HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback); + + TestRunnable testRunnable1 = new TestRunnable(); + TestRunnable testRunnable2 = new TestRunnable(); + Object messageToken = new Object(); + handler.obtainMessage(/* what= */ 1, /* obj= */ messageToken).sendToTarget(); + handler.sendEmptyMessageDelayed(/* what= */ 2, /* delayMs= */ 50); + handler.post(testRunnable1); + handler.postDelayed(testRunnable2, /* delayMs= */ 25); + handler.sendEmptyMessage(/* what= */ 3); + otherHandler.sendEmptyMessage(/* what= */ 2); + + handler.removeMessages(/* what= */ 2); + handler.removeCallbacksAndMessages(messageToken); + + fakeClock.advanceTime(50); + ShadowLooper.idleMainLooper(); + shadowOf(handlerThread.getLooper()).idle(); + + assertThat(callback.messages) + .containsExactly( + new MessageData(/* what= */ 3, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); + assertThat(testRunnable1.hasRun).isTrue(); + assertThat(testRunnable2.hasRun).isTrue(); + + // Assert that message with same "what" on other handler wasn't removed. + assertThat(otherCallback.messages) + .containsExactly( + new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); } - private static void waitForHandler(HandlerWrapper handler) { - final ConditionVariable handlerFinished = new ConditionVariable(); - handler.post(handlerFinished::open); - handlerFinished.block(); + @Test + public void createHandler_removeAllMessages_removesAllMessages() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + TestCallback callback = new TestCallback(); + HandlerWrapper handler = fakeClock.createHandler(handlerThread.getLooper(), callback); + TestCallback otherCallback = new TestCallback(); + HandlerWrapper otherHandler = fakeClock.createHandler(handlerThread.getLooper(), otherCallback); + + TestRunnable testRunnable1 = new TestRunnable(); + TestRunnable testRunnable2 = new TestRunnable(); + Object messageToken = new Object(); + handler.obtainMessage(/* what= */ 1, /* obj= */ messageToken).sendToTarget(); + handler.sendEmptyMessageDelayed(/* what= */ 2, /* delayMs= */ 50); + handler.post(testRunnable1); + handler.postDelayed(testRunnable2, /* delayMs= */ 25); + handler.sendEmptyMessage(/* what= */ 3); + otherHandler.sendEmptyMessage(/* what= */ 1); + + handler.removeCallbacksAndMessages(/* token= */ null); + + fakeClock.advanceTime(50); + ShadowLooper.idleMainLooper(); + shadowOf(handlerThread.getLooper()).idle(); + + assertThat(callback.messages).isEmpty(); + assertThat(testRunnable1.hasRun).isFalse(); + assertThat(testRunnable2.hasRun).isFalse(); + + // Assert that message on other handler wasn't removed. + assertThat(otherCallback.messages) + .containsExactly( + new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)); + } + + @Test + public void createHandler_withIsAutoAdvancing_advancesTimeToNextMessages() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true); + HandlerWrapper handler = + fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null); + + // Post a series of immediate and delayed messages. + ArrayList clockTimes = new ArrayList<>(); + handler.post( + () -> { + handler.postDelayed( + () -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 100); + handler.postDelayed(() -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 50); + handler.post(() -> clockTimes.add(fakeClock.elapsedRealtime())); + handler.postDelayed( + () -> { + clockTimes.add(fakeClock.elapsedRealtime()); + handler.postDelayed( + () -> clockTimes.add(fakeClock.elapsedRealtime()), /* delayMs= */ 50); + }, + /* delayMs= */ 20); + }); + ShadowLooper.idleMainLooper(); + shadowOf(handler.getLooper()).idle(); + + assertThat(clockTimes).containsExactly(0L, 20L, 50L, 70L, 100L).inOrder(); + } + + @Test + public void createHandler_multiThreadCommunication_deliversMessagesDeterministicallyInOrder() { + HandlerThread handlerThread1 = new HandlerThread("FakeClockTest"); + handlerThread1.start(); + HandlerThread handlerThread2 = new HandlerThread("FakeClockTest"); + handlerThread2.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0); + HandlerWrapper handler1 = + fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null); + HandlerWrapper handler2 = + fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null); + + ConditionVariable messagesFinished = new ConditionVariable(); + ArrayList executionOrder = new ArrayList<>(); + handler1.post( + () -> { + executionOrder.add(1); + handler2.post(() -> executionOrder.add(2)); + handler1.post(() -> executionOrder.add(3)); + handler2.post( + () -> { + executionOrder.add(4); + handler2.post(() -> executionOrder.add(7)); + handler1.post( + () -> { + executionOrder.add(8); + messagesFinished.open(); + }); + }); + handler2.post(() -> executionOrder.add(5)); + handler1.post(() -> executionOrder.add(6)); + }); + ShadowLooper.idleMainLooper(); + messagesFinished.block(); + + assertThat(executionOrder).containsExactly(1, 2, 3, 4, 5, 6, 7, 8).inOrder(); + } + + @Test + public void createHandler_blockingThreadWithOnBusyWaiting_canBeUnblockedByOtherThread() { + HandlerThread handlerThread1 = new HandlerThread("FakeClockTest"); + handlerThread1.start(); + HandlerThread handlerThread2 = new HandlerThread("FakeClockTest"); + handlerThread2.start(); + FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, /* isAutoAdvancing= */ true); + HandlerWrapper handler1 = + fakeClock.createHandler(handlerThread1.getLooper(), /* callback= */ null); + HandlerWrapper handler2 = + fakeClock.createHandler(handlerThread2.getLooper(), /* callback= */ null); + + ArrayList executionOrder = new ArrayList<>(); + handler1.post( + () -> { + executionOrder.add(1); + ConditionVariable blockingCondition = new ConditionVariable(); + handler2.postDelayed( + () -> { + executionOrder.add(2); + blockingCondition.open(); + }, + /* delayMs= */ 50); + handler1.post(() -> executionOrder.add(4)); + fakeClock.onThreadBlocked(); + blockingCondition.block(); + executionOrder.add(3); + }); + ShadowLooper.idleMainLooper(); + shadowOf(handler1.getLooper()).idle(); + shadowOf(handler2.getLooper()).idle(); + + assertThat(executionOrder).containsExactly(1, 2, 3, 4).inOrder(); } private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) { @@ -158,48 +377,6 @@ public final class FakeClockTest { } } - private static final class SleeperThread extends Thread { - - private final Clock clock; - private final long sleepDurationMs; - private final CountDownLatch fallAsleepCountDownLatch; - private final CountDownLatch wakeUpCountDownLatch; - - private volatile boolean isSleeping; - - public SleeperThread(Clock clock, long sleepDurationMs) { - this.clock = clock; - this.sleepDurationMs = sleepDurationMs; - this.fallAsleepCountDownLatch = new CountDownLatch(1); - this.wakeUpCountDownLatch = new CountDownLatch(1); - } - - public boolean waitUntilAsleep(long timeoutMs) throws InterruptedException { - return fallAsleepCountDownLatch.await(timeoutMs, MILLISECONDS); - } - - public boolean waitUntilAwake(long timeoutMs) throws InterruptedException { - return wakeUpCountDownLatch.await(timeoutMs, MILLISECONDS); - } - - public boolean isSleeping() { - return isSleeping; - } - - @Override - public void run() { - // This relies on the FakeClock's methods synchronizing on its own monitor to ensure that - // any interactions with it occur only after sleep() has called wait() or returned. - synchronized (clock) { - isSleeping = true; - fallAsleepCountDownLatch.countDown(); - clock.sleep(sleepDurationMs); - isSleeping = false; - wakeUpCountDownLatch.countDown(); - } - } - } - private static final class TestRunnable implements Runnable { public boolean hasRun; @@ -209,4 +386,54 @@ public final class FakeClockTest { hasRun = true; } } + + private static final class TestCallback implements Handler.Callback { + + public final List messages; + + public TestCallback() { + messages = new ArrayList<>(); + } + + @Override + public boolean handleMessage(@NonNull Message msg) { + messages.add(new MessageData(msg.what, msg.arg1, msg.arg2, msg.obj)); + return true; + } + } + + private static final class MessageData { + + public final int what; + public final int arg1; + public final int arg2; + @Nullable public final Object obj; + + public MessageData(int what, int arg1, int arg2, @Nullable Object obj) { + this.what = what; + this.arg1 = arg1; + this.arg2 = arg2; + this.obj = obj; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MessageData)) { + return false; + } + MessageData that = (MessageData) o; + return what == that.what + && arg1 == that.arg1 + && arg2 == that.arg2 + && Objects.equal(obj, that.obj); + } + + @Override + public int hashCode() { + return Objects.hashCode(what, arg1, arg2, obj); + } + } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/WebServerDispatcherTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/WebServerDispatcherTest.java index 28962eaf2b..78418565b6 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/WebServerDispatcherTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/WebServerDispatcherTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.Arrays; import okhttp3.OkHttpClient; @@ -29,6 +30,26 @@ import org.junit.Test; import org.junit.runner.RunWith; /** Tests for {@link WebServerDispatcher}. */ +// We use the OkHttp client library for these tests because it's generally nicer to use than Java's +// HttpURLConnection. +// +// However, OkHttp's 'transparent compression' behaviour is annoying when trying to test the edge +// cases of the WebServerDispatcher's Accept-Encoding header handling. If passed a request with no +// Accept-Encoding header, the OkHttp client library will silently add one that accepts gzip and +// then silently unzip the response data (and remove the Content-Coding header) before returning it. +// +// This gets in the way of some test cases, for example testing how the WebServerDispatcher handles +// a request with *no* Accept-Encoding header (since it's impossible to send this using OkHttp). +// +// Under Robolectric, the Java HttpURLConnection doesn't have this transparent compression +// behaviour, but that's a Robolectric 'bug' (internal: b/177071755) because the Android platform +// implementation of HttpURLConnection does (it uses OkHttp under the hood). So we can't really use +// HttpURLConnection to test these edge cases either (even though it would work for now) because +// ideally Robolectric will in the future make the implementation more realistic and suddenly our +// tests would be wrong. +// +// So instead we just don't test these cases that require passing header combinations that are +// impossible with OkHttp. @RunWith(AndroidJUnit4.class) public class WebServerDispatcherTest { @@ -49,6 +70,10 @@ public class WebServerDispatcherTest { "/range/requests/not-supported-length-unknown"; private static final byte[] RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA = TestUtil.buildTestData(/* length= */ 20, seed++); + private static final String GZIP_ENABLED_PATH = "/gzip/enabled"; + private static final byte[] GZIP_ENABLED_DATA = TestUtil.buildTestData(/* length= */ 20, seed++); + private static final String GZIP_FORCED_PATH = "/gzip/forced"; + private static final byte[] GZIP_FORCED_DATA = TestUtil.buildTestData(/* length= */ 20, seed++); private MockWebServer mockWebServer; @@ -79,6 +104,16 @@ public class WebServerDispatcherTest { .setData(RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA) .supportsRangeRequests(false) .resolvesToUnknownLength(true) + .build(), + new WebServerDispatcher.Resource.Builder() + .setPath(GZIP_ENABLED_PATH) + .setData(GZIP_ENABLED_DATA) + .setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED) + .build(), + new WebServerDispatcher.Resource.Builder() + .setPath(GZIP_FORCED_PATH) + .setData(GZIP_FORCED_DATA) + .setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED) .build()))); } @@ -392,4 +427,155 @@ public class WebServerDispatcherTest { assertThat(response.body().bytes()).isEqualTo(RANGE_UNSUPPORTED_DATA); } } + + @Test + public void gzipDisabled_acceptEncodingHeaderAllowsAnyCoding_identityResponse() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(RANGE_SUPPORTED_PATH)) + .addHeader("Accept-Encoding", "*") + .build(); + try (Response response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("identity"); + assertThat(response.header("Content-Length")) + .isEqualTo(String.valueOf(RANGE_SUPPORTED_DATA.length)); + assertThat(response.body().bytes()).isEqualTo(RANGE_SUPPORTED_DATA); + } + } + + @Test + public void gzipDisabled_acceptEncodingHeaderRequiresGzip_406Response() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(RANGE_SUPPORTED_PATH)) + .addHeader("Accept-Encoding", "gzip;q=1.0") + .addHeader("Accept-Encoding", "identity;q=0") + .build(); + try (Response response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(406); + } + } + + @Test + public void gzipDisabled_acceptEncodingHeaderRequiresGzipViaAsterisk_406Response() + throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(RANGE_SUPPORTED_PATH)) + .addHeader("Accept-Encoding", "gzip;q=1.0") + .addHeader("Accept-Encoding", "*;q=0") + .build(); + try (Response response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(406); + } + } + + @Test + public void gzipEnabled_acceptEncodingHeaderAllowsGzip_responseGzipped() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(GZIP_ENABLED_PATH)) + .addHeader("Accept-Encoding", "gzip") + .build(); + try (Response response = client.newCall(request).execute()) { + byte[] expectedData = Util.gzip(GZIP_ENABLED_DATA); + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("gzip"); + assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length)); + assertThat(response.body().bytes()).isEqualTo(expectedData); + } + } + + @Test + public void gzipEnabled_acceptEncodingHeaderAllowsAnyCoding_responseGzipped() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(GZIP_ENABLED_PATH)) + .addHeader("Accept-Encoding", "*") + .build(); + try (Response response = client.newCall(request).execute()) { + byte[] expectedData = Util.gzip(GZIP_ENABLED_DATA); + + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("gzip"); + assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length)); + assertThat(response.body().bytes()).isEqualTo(expectedData); + } + } + + @Test + public void gzipEnabled_acceptEncodingHeaderPrefersIdentity_responseNotGzipped() + throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(GZIP_ENABLED_PATH)) + .addHeader("Accept-Encoding", "identity;q=0.8, gzip;q=0.2") + .build(); + try (Response response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("identity"); + assertThat(response.header("Content-Length")) + .isEqualTo(String.valueOf(GZIP_ENABLED_DATA.length)); + assertThat(response.body().bytes()).isEqualTo(GZIP_ENABLED_DATA); + } + } + + @Test + public void gzipEnabled_acceptEncodingHeaderExcludesGzip_responseNotGzipped() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(GZIP_ENABLED_PATH)) + .addHeader("Accept-Encoding", "identity") + .build(); + try (Response response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("identity"); + assertThat(response.header("Content-Length")) + .isEqualTo(String.valueOf(GZIP_ENABLED_DATA.length)); + assertThat(response.body().bytes()).isEqualTo(GZIP_ENABLED_DATA); + } + } + + @Test + public void gzipForced_acceptEncodingHeaderAllowsGzip_responseGzipped() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(GZIP_FORCED_PATH)) + .addHeader("Accept-Encoding", "gzip") + .build(); + try (Response response = client.newCall(request).execute()) { + byte[] expectedData = Util.gzip(GZIP_FORCED_DATA); + + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("gzip"); + assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length)); + assertThat(response.body().bytes()).isEqualTo(expectedData); + } + } + + @Test + public void gzipForced_acceptEncodingHeaderExcludesGzip_responseNotGzipped() throws Exception { + OkHttpClient client = new OkHttpClient(); + Request request = + new Request.Builder() + .url(mockWebServer.url(GZIP_FORCED_PATH)) + .addHeader("Accept-Encoding", "identity") + .build(); + try (Response response = client.newCall(request).execute()) { + assertThat(response.code()).isEqualTo(200); + assertThat(response.header("Content-Encoding")).isEqualTo("identity"); + assertThat(response.header("Content-Length")) + .isEqualTo(String.valueOf(GZIP_FORCED_DATA.length)); + assertThat(response.body().bytes()).isEqualTo(GZIP_FORCED_DATA); + } + } }