From 8955cd3a61fe00f2f44f10f62a104175c851d5c0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Sep 2020 19:30:48 +0100 Subject: [PATCH 01/69] Clean up experimental offload Javadoc PiperOrigin-RevId: 331591005 --- .../java/com/google/android/exoplayer2/ExoPlayer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index b5489186bc8..ccb67866a41 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 @@ -16,11 +16,13 @@ package com.google.android.exoplayer2; import android.content.Context; +import android.media.AudioTrack; import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -622,14 +624,13 @@ public ExoPlayer build() { * the following: * * * *

This method is experimental, and will be renamed or removed in a future release. From 97a0df77f6932f1a65f4bbe0eb2f800a47d84f9b Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 9 Sep 2020 17:23:05 +0100 Subject: [PATCH 02/69] Support android.resource URI scheme Issue: #7866 PiperOrigin-RevId: 330736774 --- RELEASENOTES.md | 6 ++ .../upstream/DefaultDataSource.java | 10 +- .../upstream/RawResourceDataSource.java | 92 +++++++++++++------ 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index aa61bbe0e63..61a4a6900c0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,11 @@ # Release notes +### 2.12.1 ### + +* Data sources: + * Add support for `android.resource` URI scheme in `RawResourceDataSource` + ([#7866](https://github.com/google/ExoPlayer/issues/7866)). + ### 2.12.0 (2020-09-11) ### To learn more about what's new in 2.12, read the corresponding diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 7efa89eaa0d..12fea3898cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import androidx.annotation.Nullable; @@ -39,6 +40,9 @@ *

  • rawresource: For fetching data from a raw resource in the application's apk (e.g. * rawresource:///resourceId, where rawResourceId is the integer identifier of the raw * resource). + *
  • android.resource: For fetching data in the application's apk (e.g. + * android.resource:///resourceId or android.resource://resourceType/resourceName). See {@link + * RawResourceDataSource} for more information about the URI form. *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). *
  • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension. @@ -58,7 +62,9 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; private static final String SCHEME_UDP = "udp"; + private static final String SCHEME_DATA = DataSchemeDataSource.SCHEME_DATA; private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; + private static final String SCHEME_ANDROID_RESOURCE = ContentResolver.SCHEME_ANDROID_RESOURCE; private final Context context; private final List transferListeners; @@ -182,9 +188,9 @@ public long open(DataSpec dataSpec) throws IOException { dataSource = getRtmpDataSource(); } else if (SCHEME_UDP.equals(scheme)) { dataSource = getUdpDataSource(); - } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { + } else if (SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); - } else if (SCHEME_RAW.equals(scheme)) { + } else if (SCHEME_RAW.equals(scheme) || SCHEME_ANDROID_RESOURCE.equals(scheme)) { dataSource = getRawResourceDataSource(); } else { dataSource = baseDataSource; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 0595cb84bc5..7538cc67a49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -18,6 +18,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import static java.lang.Math.min; +import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -34,9 +35,20 @@ /** * A {@link DataSource} for reading a raw resource inside the APK. * - *

    URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where - * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can - * be used to build {@link Uri}s in this format. + *

    URIs supported by this source are of one of the forms: + * + *

      + *
    • {@code rawresource:///id}, where {@code id} is the integer identifier of a raw resource. + *
    • {@code android.resource:///id}, where {@code id} is the integer identifier of a raw + * resource. + *
    • {@code android.resource://[package]/[type/]name}, where {@code package} is the name of the + * package in which the resource is located, {@code type} is the resource type and {@code + * name} is the resource name. The package and the type are optional. Their default value is + * the package of this application and "raw", respectively. Using the two other forms is more + * efficient. + *
    + * + *

    {@link #buildRawResourceUri(int)} can be used to build supported {@link Uri}s. */ public final class RawResourceDataSource extends BaseDataSource { @@ -67,6 +79,7 @@ public static Uri buildRawResourceUri(int rawResourceId) { public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; + private final String packageName; @Nullable private Uri uri; @Nullable private AssetFileDescriptor assetFileDescriptor; @@ -80,33 +93,55 @@ public static Uri buildRawResourceUri(int rawResourceId) { public RawResourceDataSource(Context context) { super(/* isNetwork= */ false); this.resources = context.getResources(); + this.packageName = context.getPackageName(); } @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { - try { - Uri uri = dataSpec.uri; - this.uri = uri; - if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) { - throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME); - } - - int resourceId; + Uri uri = dataSpec.uri; + this.uri = uri; + + int resourceId; + if (TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme()) + || (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme()) + && uri.getPathSegments().size() == 1 + && Assertions.checkNotNull(uri.getLastPathSegment()).matches("\\d+"))) { try { resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment())); } catch (NumberFormatException e) { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } - - transferInitializing(dataSpec); - AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); - this.assetFileDescriptor = assetFileDescriptor; - if (assetFileDescriptor == null) { - throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } else if (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme())) { + String path = Assertions.checkNotNull(uri.getPath()); + if (path.startsWith("/")) { + path = path.substring(1); + } + @Nullable String host = uri.getHost(); + String resourceName = (TextUtils.isEmpty(host) ? "" : (host + ":")) + path; + resourceId = + resources.getIdentifier( + resourceName, /* defType= */ "raw", /* defPackage= */ packageName); + if (resourceId == 0) { + throw new RawResourceDataSourceException("Resource not found."); } - FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - this.inputStream = inputStream; + } else { + throw new RawResourceDataSourceException( + "URI must either use scheme " + + RAW_RESOURCE_SCHEME + + " or " + + ContentResolver.SCHEME_ANDROID_RESOURCE); + } + + transferInitializing(dataSpec); + AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + try { inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -114,18 +149,21 @@ public long open(DataSpec dataSpec) throws RawResourceDataSourceException { // skip beyond the end of the data. throw new EOFException(); } - if (dataSpec.length != C.LENGTH_UNSET) { - bytesRemaining = dataSpec.length; - } else { - long assetFileDescriptorLength = assetFileDescriptor.getLength(); - // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file. - bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH - ? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position); - } } catch (IOException e) { throw new RawResourceDataSourceException(e); } + if (dataSpec.length != C.LENGTH_UNSET) { + bytesRemaining = dataSpec.length; + } else { + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file. + bytesRemaining = + assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH + ? C.LENGTH_UNSET + : (assetFileDescriptorLength - dataSpec.position); + } + opened = true; transferStarted(dataSpec); From f4896d769f661cc6d6c376e593c0e310c0b01b2a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 16 Sep 2020 16:43:45 +0100 Subject: [PATCH 03/69] Replace duration strings with plurals PiperOrigin-RevId: 332014290 --- .../exoplayer2/ui/StyledPlayerControlView.java | 13 ++++++++----- library/ui/src/main/res/values/strings.xml | 10 ++++++++-- library/ui/src/main/res/values/styles.xml | 2 -- 3 files changed, 16 insertions(+), 9 deletions(-) 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 97652ad01f7..8bb9babeb0b 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 @@ -1159,13 +1159,14 @@ private void updateRewindButton() { if (controlDispatcher instanceof DefaultControlDispatcher) { rewindMs = ((DefaultControlDispatcher) controlDispatcher).getRewindIncrementMs(); } - long rewindSec = rewindMs / 1_000; + int rewindSec = (int) (rewindMs / 1_000); if (rewindButtonTextView != null) { rewindButtonTextView.setText(String.valueOf(rewindSec)); } if (rewindButton != null) { rewindButton.setContentDescription( - resources.getString(R.string.exo_controls_rewind_by_amount_description, rewindSec)); + resources.getQuantityString( + R.plurals.exo_controls_rewind_by_amount_description, rewindSec, rewindSec)); } } @@ -1173,14 +1174,16 @@ private void updateFastForwardButton() { if (controlDispatcher instanceof DefaultControlDispatcher) { fastForwardMs = ((DefaultControlDispatcher) controlDispatcher).getFastForwardIncrementMs(); } - long fastForwardSec = fastForwardMs / 1_000; + int fastForwardSec = (int) (fastForwardMs / 1_000); if (fastForwardButtonTextView != null) { fastForwardButtonTextView.setText(String.valueOf(fastForwardSec)); } if (fastForwardButton != null) { fastForwardButton.setContentDescription( - resources.getString( - R.string.exo_controls_fastforward_by_amount_description, fastForwardSec)); + resources.getQuantityString( + R.plurals.exo_controls_fastforward_by_amount_description, + fastForwardSec, + fastForwardSec)); } } diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index a65d81e2b12..a11d04073f6 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -43,11 +43,17 @@ Rewind - Rewind %d seconds + + Rewind %d second + Rewind %d seconds + Fast forward - Fast forward %d seconds + + Fast forward %d second + Fast forward %d seconds + Repeat none diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 76ea27ef5a9..f903441698a 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -90,14 +90,12 @@ - From 6a9f125c149a93851aa970cd762fb837814a41b4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 09:43:23 +0100 Subject: [PATCH 10/69] Don't assume FakeSampleStream is ended without end of stream signal PiperOrigin-RevId: 333029935 --- .../android/exoplayer2/testutil/FakeSampleStream.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 7d63e129dbc..4f4aee96754 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -190,7 +190,12 @@ public void seekTo(long timeUs) { } } sampleItemIndex = fakeSampleStreamItems.size(); - readEOSBuffer = true; + if (!fakeSampleStreamItems.isEmpty()) { + FakeSampleStreamItem lastItem = Iterables.getLast(fakeSampleStreamItems); + readEOSBuffer = + lastItem.sampleInfo != null + && ((lastItem.sampleInfo.flags & C.BUFFER_FLAG_END_OF_STREAM) != 0); + } } /** From 1b5d07c10536bc64a78b38796e958f9e8a0da423 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 09:56:14 +0100 Subject: [PATCH 11/69] Guava-ify https://github.com/google/ExoPlayer/commit/f2c51560c21bdd757c30678223345fa8f59fb82b PiperOrigin-RevId: 333031301 --- .../exoplayer2/testutil/FakeSampleStream.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 4f4aee96754..eaa2fb52bb0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -190,12 +190,13 @@ public void seekTo(long timeUs) { } } sampleItemIndex = fakeSampleStreamItems.size(); - if (!fakeSampleStreamItems.isEmpty()) { - FakeSampleStreamItem lastItem = Iterables.getLast(fakeSampleStreamItems); - readEOSBuffer = - lastItem.sampleInfo != null - && ((lastItem.sampleInfo.flags & C.BUFFER_FLAG_END_OF_STREAM) != 0); - } + @Nullable + FakeSampleStreamItem lastItem = + Iterables.getLast(fakeSampleStreamItems, /* defaultValue= */ null); + readEOSBuffer = + lastItem != null + && lastItem.sampleInfo != null + && ((lastItem.sampleInfo.flags & C.BUFFER_FLAG_END_OF_STREAM) != 0); } /** From b6036561c27284bc618fb5e8b6ca0497328a91d5 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 22 Sep 2020 09:57:16 +0100 Subject: [PATCH 12/69] Exclude PC devices from H.265 GTS tests PiperOrigin-RevId: 333031399 --- .../playbacktests/gts/DashStreamingTest.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index a2f557ca0de..c3e82ec33da 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -18,6 +18,7 @@ import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest; import static com.google.common.truth.Truth.assertThat; +import android.content.pm.PackageManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import com.google.android.exoplayer2.ExoPlayer; @@ -168,7 +169,7 @@ public void h264AdaptiveWithRendererDisabling() throws Exception { @Test public void h265FixedV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || isPc()) { // Pass. return; } @@ -183,7 +184,7 @@ public void h265FixedV23() throws Exception { @Test public void h265AdaptiveV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -199,7 +200,7 @@ public void h265AdaptiveV24() throws Exception { @Test public void h265AdaptiveWithSeekingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -216,7 +217,7 @@ public void h265AdaptiveWithSeekingV24() throws Exception { @Test public void h265AdaptiveWithRendererDisablingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -435,7 +436,7 @@ public void widevineH264AdaptiveWithRendererDisablingV18() throws Exception { @Test public void widevineH265FixedV23() throws Exception { - if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -452,7 +453,7 @@ public void widevineH265FixedV23() throws Exception { @Test public void widevineH265AdaptiveV24() throws Exception { - if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -469,7 +470,7 @@ public void widevineH265AdaptiveV24() throws Exception { @Test public void widevineH265AdaptiveWithSeekingV24() throws Exception { - if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -487,7 +488,7 @@ public void widevineH265AdaptiveWithSeekingV24() throws Exception { @Test public void widevineH265AdaptiveWithRendererDisablingV24() throws Exception { - if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -644,7 +645,7 @@ public void decoderInfoH264() throws Exception { @Test public void decoderInfoH265V24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -670,6 +671,11 @@ public void decoderInfoVP9V24() throws Exception { // Internal. + private boolean isPc() { + // See [internal b/162990153]. + return testRule.getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC); + } + private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException { MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, /* secure= */ false, /* tunneling= */ false); From 93948471c8d870cabdced0526ec398f09f753641 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 12:23:24 +0100 Subject: [PATCH 13/69] Don't require the existence of the next period to wait for its stream. We have a workaround for uneven sample stream durarions in playlists that assumes a renderer allows playback if it's reading ahead or waiting for the next stream. https://github.com/google/ExoPlayer/commit/652c2f9c188bf9d9d6e323ff5333e5026454a082 changed this logic to no longer require to wait until the next stream is prepared due to a change in how we advance media periods in the queue. However, the code falsely still requires the next stream to exist (even if it's not prepared). This can cause a stuck buffering state when the difference in the duration of the streams is more than what we buffer ahead because we never create the next stream in such a case. Note: DefaultMediaClock.shouldUseStandaloneClock has roughly the same logic and also doesn't require the next stream to be present. Also fix a test that seemed to rely on this stuck buffering case to test stuck buffering detection. Changed the test to not read the end of stream to ensure it runs into the desired stuck buffering case. Issue:#7943 PiperOrigin-RevId: 333050285 --- RELEASENOTES.md | 4 ++++ .../android/exoplayer2/ExoPlayerImplInternal.java | 5 +---- .../google/android/exoplayer2/ExoPlayerTest.java | 15 +++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6c382f87245..dbeb77236ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### 2.12.1 ### +* Core library: + * Fix bug where streams with highly uneven durations may get stuck in a + buffering state + ([#7943](https://github.com/google/ExoPlayer/issues/7943)). * Data sources: * Add support for `android.resource` URI scheme in `RawResourceDataSource` ([#7866](https://github.com/google/ExoPlayer/issues/7866)). 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 9739680e79a..e33b93ac0ef 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 @@ -897,10 +897,7 @@ private void doSomeWork() throws ExoPlaybackException, IOException { // tracks in the current period have uneven durations and are still being read by another // renderer. See: https://github.com/google/ExoPlayer/issues/1874. boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); - boolean isWaitingForNextStream = - !isReadingAhead - && playingPeriodHolder.getNext() != null - && renderer.hasReadStreamToEnd(); + boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd(); boolean allowsPlayback = isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; 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 444640256f6..7934298df08 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 @@ -7334,6 +7334,8 @@ public void run(SimpleExoPlayer player) { new DefaultLoadControl.Builder() .setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE) .build(); + // Return no end of stream signal to prevent playback from ending. + FakeMediaPeriod.TrackDataFactory trackDataWithoutEos = (format, periodId) -> ImmutableList.of(); MediaSource continuouslyAllocatingMediaSource = new FakeMediaSource( new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { @@ -7348,8 +7350,11 @@ protected FakeMediaPeriod createFakeMediaPeriod( @Nullable TransferListener transferListener) { return new FakeMediaPeriod( trackGroupArray, - TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, - mediaSourceEventDispatcher) { + trackDataWithoutEos, + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + /* deferOnPrepared= */ false) { private final List allocations = new ArrayList<>(); @@ -7382,14 +7387,8 @@ public boolean continueLoading(long positionUs) { }; } }; - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - // Prevent player from ever assuming it finished playing. - .setRepeatMode(Player.REPEAT_MODE_ALL) - .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder(context) - .setActionSchedule(actionSchedule) .setMediaSources(continuouslyAllocatingMediaSource) .setLoadControl(loadControl) .build(); From 884a0f52cd91d70be6fdfd06b3229842cdf7eb34 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 22 Sep 2020 12:27:58 +0100 Subject: [PATCH 14/69] Remove unused MP4 atom type PiperOrigin-RevId: 333051018 --- .../java/com/google/android/exoplayer2/extractor/mp4/Atom.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index e86a873ed56..91b26562ca8 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -274,9 +274,6 @@ @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_TTML = 0x54544d4c; - @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vmhd = 0x766d6864; - @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_mp4v = 0x6d703476; From 79638d1e3d870125b7f0ea67c93985e96d0d1f33 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 23 Sep 2020 13:22:27 +0100 Subject: [PATCH 15/69] Add support for 'mett' sample description PiperOrigin-RevId: 333272292 --- .../android/exoplayer2/extractor/mp4/Atom.java | 3 +++ .../exoplayer2/extractor/mp4/AtomParsers.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 91b26562ca8..58f3a75b874 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -355,6 +355,9 @@ @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_camm = 0x63616d6d; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mett = 0x6d657474; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_alac = 0x616c6163; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 6eed09760e0..0ab126367b0 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -891,6 +891,8 @@ private static StsdData parseStsd( || childAtomType == Atom.TYPE_c608) { parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, out); + } else if (childAtomType == Atom.TYPE_mett) { + parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, trackId, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = new Format.Builder() @@ -1097,6 +1099,18 @@ private static void parseVideoSampleEntry( .build(); } + private static void parseMetaDataSampleEntry( + ParsableByteArray parent, int atomType, int position, int trackId, StsdData out) { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + if (atomType == Atom.TYPE_mett) { + parent.readNullTerminatedString(); // Skip optional content_encoding + @Nullable String mimeType = parent.readNullTerminatedString(); + if (mimeType != null) { + out.format = new Format.Builder().setId(trackId).setSampleMimeType(mimeType).build(); + } + } + } + /** * Parses the edts atom (defined in ISO/IEC 14496-12 subsection 8.6.5). * From df1619fa3f37ac1e6568097c0e05403fcbbddb5f Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 23 Sep 2020 16:27:24 +0100 Subject: [PATCH 16/69] Add Japanese subtitle examples to the demo app These are from https://medium.com/google-exoplayer/improved-japanese-subtitle-support-7598fee12cf4 PiperOrigin-RevId: 333296789 --- demos/main/src/main/assets/media.exolist.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index ce1854db852..24213918f50 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -527,6 +527,20 @@ { "name": "MPEG-4 Timed Text (tx3g, mov_text)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4" + }, + { + "name": "Japanese features (vertical + rubies) [TTML]", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml", + "subtitle_mime_type": "application/ttml+xml", + "subtitle_language": "ja" + }, + { + "name": "Japanese features (vertical + rubies) [WebVTT]", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt", + "subtitle_mime_type": "text/vtt", + "subtitle_language": "ja" } ] }, From 92f9de8ea3a0f217864e79deb8a584d1c29fb309 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 24 Sep 2020 10:00:46 +0100 Subject: [PATCH 17/69] Delete sample_cbs.adts test asset This seems to be an exact copy of sample.adts. Update the test to use the same sample but just output to a different dump file. PiperOrigin-RevId: 333469714 --- .../extractor/ts/AdtsExtractorTest.java | 6 +++++- .../src/test/assets/media/ts/sample_cbs.adts | Bin 31805 -> 0 bytes 2 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 testdata/src/test/assets/media/ts/sample_cbs.adts diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index e8bc727222e..dca8ba99383 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,7 +50,10 @@ public void sample_with_id3() throws Exception { public void sample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), - "media/ts/sample_cbs.adts", + "media/ts/sample.adts", + new AssertionConfig.Builder() + .setDumpFilesPrefix("extractordumps/ts/sample_cbs.adts") + .build(), simulationConfig); } diff --git a/testdata/src/test/assets/media/ts/sample_cbs.adts b/testdata/src/test/assets/media/ts/sample_cbs.adts deleted file mode 100644 index abbaad0daf95cf2bce299b161a2ea6d658b587e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31805 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∈f1 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 From 64aa634f9bb8a64b95b32888a05e59f17cd36c31 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 24 Sep 2020 10:19:11 +0100 Subject: [PATCH 18/69] Fix offload buffer full detection after setEndOfStream This issue has been observed on a test app stress testing setEndOfStream. The issue has not been observed on ExoPlayer, probably due to timing differences, but it is fixed preventively. #exo-offload PiperOrigin-RevId: 333472136 --- .../exoplayer2/audio/DefaultAudioSink.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 41e76440c11..478eb0d04b2 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 @@ -336,6 +336,7 @@ public long getSkippedOutputFrameCount() { private boolean tunneling; private long lastFeedElapsedRealtimeMs; private boolean offloadDisabledUntilNextConfiguration; + private boolean isWaitingForOffloadEndOfStreamHandled; /** * Creates a new default audio sink. @@ -712,6 +713,7 @@ public boolean handleBuffer( audioTrack.setOffloadEndOfStream(); audioTrack.setOffloadDelayPadding( configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding); + isWaitingForOffloadEndOfStreamHandled = true; } } // Re-apply playback parameters. @@ -932,13 +934,26 @@ private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throw throw new WriteException(bytesWritten); } - if (playing - && listener != null - && bytesWritten < bytesRemaining - && isOffloadedPlayback(audioTrack)) { - long pendingDurationMs = - audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames); - listener.onOffloadBufferFull(pendingDurationMs); + if (isOffloadedPlayback(audioTrack)) { + // After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and + // restarts during which AudioTrack.write will return 0. This situation must be detected to + // prevent reporting the buffer as full even though it is not which could lead ExoPlayer to + // sleep forever waiting for a onDataRequest that will never come. + if (writtenEncodedFrames > 0) { + isWaitingForOffloadEndOfStreamHandled = false; + } + + // Consider the offload buffer as full if the AudioTrack is playing and AudioTrack.write could + // not write all the data provided to it. This relies on the assumption that AudioTrack.write + // always writes as much as possible. + if (playing + && listener != null + && bytesWritten < bytesRemaining + && !isWaitingForOffloadEndOfStreamHandled) { + long pendingDurationMs = + audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames); + listener.onOffloadBufferFull(pendingDurationMs); + } } if (configuration.outputMode == OUTPUT_MODE_PCM) { @@ -1221,6 +1236,7 @@ private void resetSinkStateForFlush() { submittedEncodedFrames = 0; writtenPcmBytes = 0; writtenEncodedFrames = 0; + isWaitingForOffloadEndOfStreamHandled = false; framesPerEncodedSample = 0; mediaPositionParameters = new MediaPositionParameters( From 30be792a15d16d32bd0c2bc82e64de8e74199acb Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 24 Sep 2020 11:28:24 +0100 Subject: [PATCH 19/69] Switch SntpClient to time.android.com and allow to set host. PiperOrigin-RevId: 333480727 --- .../android/exoplayer2/util/SntpClient.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java index 19159ede6ed..03336fdeba8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java @@ -27,6 +27,7 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Arrays; +import java.util.ConcurrentModificationException; /** * Static utility to retrieve the device time offset using SNTP. @@ -37,6 +38,9 @@ */ public final class SntpClient { + /** The default NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. */ + public static final String DEFAULT_NTP_HOST = "time.android.com"; + /** Callback for calls to {@link #initialize(Loader, InitializationCallback)}. */ public interface InitializationCallback { @@ -51,7 +55,6 @@ public interface InitializationCallback { void onInitializationFailed(IOException error); } - private static final String NTP_HOST = "pool.ntp.org"; private static final int TIMEOUT_MS = 10_000; private static final int ORIGINATE_TIME_OFFSET = 24; @@ -80,8 +83,37 @@ public interface InitializationCallback { @GuardedBy("valueLock") private static long elapsedRealtimeOffsetMs; + @GuardedBy("valueLock") + private static String ntpHost = DEFAULT_NTP_HOST; + private SntpClient() {} + /** Returns the NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. */ + public static String getNtpHost() { + synchronized (valueLock) { + return ntpHost; + } + } + + /** + * Sets the NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. + * + *

    The default is {@link #DEFAULT_NTP_HOST}. + * + *

    If the new host address is different from the previous one, the NTP client will be {@link + * #isInitialized()} uninitialized} again. + * + * @param ntpHost The NTP host address. + */ + public static void setNtpHost(String ntpHost) { + synchronized (valueLock) { + if (!SntpClient.ntpHost.equals(ntpHost)) { + SntpClient.ntpHost = ntpHost; + isInitialized = false; + } + } + } + /** * Returns whether the device time offset has already been loaded. * @@ -129,7 +161,7 @@ public static void initialize( } private static long loadNtpTimeOffsetMs() throws IOException { - InetAddress address = InetAddress.getByName(NTP_HOST); + InetAddress address = InetAddress.getByName(getNtpHost()); try (DatagramSocket socket = new DatagramSocket()) { socket.setSoTimeout(TIMEOUT_MS); byte[] buffer = new byte[NTP_PACKET_SIZE]; @@ -282,9 +314,14 @@ public NtpTimeCallback(@Nullable InitializationCallback callback) { @Override public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - Assertions.checkState(SntpClient.isInitialized()); if (callback != null) { - callback.onInitialized(); + if (!SntpClient.isInitialized()) { + // This may happen in the unlikely edge case of someone calling setNtpHost between the end + // of the load method and this callback. + callback.onInitializationFailed(new IOException(new ConcurrentModificationException())); + } else { + callback.onInitialized(); + } } } From 31251a40c1c504c78874a501a7e5e872ae68485b Mon Sep 17 00:00:00 2001 From: kim-vde Date: Fri, 25 Sep 2020 16:31:49 +0100 Subject: [PATCH 20/69] Merge pull request #7968 from DolbyLaboratories:dev-v2-channelConfiguration PiperOrigin-RevId: 333485323 --- .../dash/manifest/DashManifestParser.java | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index e9e9c66df2b..8015c139b40 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -70,6 +70,16 @@ public class DashManifestParser extends DefaultHandler private static final Pattern CEA_708_ACCESSIBILITY_PATTERN = Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*"); + /** + * Maps the value attribute of an AudioElementConfiguration with schemeIdUri + * "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1, to a channel + * count. + */ + private static final int[] MPEG_CHANNEL_CONFIGURATION_MAPPING = + new int[] { + Format.NO_VALUE, 1, 2, 3, 4, 5, 6, 8, 2, 3, 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, 14 + }; + private final XmlPullParserFactory xmlParserFactory; public DashManifestParser() { @@ -1156,13 +1166,22 @@ protected String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) protected int parseAudioChannelConfiguration(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); - int audioChannels = - "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseInt(xpp, "value", Format.NO_VALUE) - : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) - || "urn:dolby:dash:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseDolbyChannelConfiguration(xpp) - : Format.NO_VALUE); + int audioChannels; + switch (schemeIdUri) { + case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011": + audioChannels = parseInt(xpp, "value", Format.NO_VALUE); + break; + case "urn:mpeg:mpegB:cicp:ChannelConfiguration": + audioChannels = parseMpegChannelConfiguration(xpp); + break; + case "tag:dolby.com,2014:dash:audio_channel_configuration:2011": + case "urn:dolby:dash:audio_channel_configuration:2011": + audioChannels = parseDolbyChannelConfiguration(xpp); + break; + default: + audioChannels = Format.NO_VALUE; + break; + } do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); @@ -1528,6 +1547,21 @@ protected static String parseString(XmlPullParser xpp, String name, String defau return value == null ? defaultValue : value; } + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1. + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseMpegChannelConfiguration(XmlPullParser xpp) { + int index = parseInt(xpp, "value", C.INDEX_UNSET); + return 0 <= index && index < MPEG_CHANNEL_CONFIGURATION_MAPPING.length + ? MPEG_CHANNEL_CONFIGURATION_MAPPING[index] + : Format.NO_VALUE; + } + /** * Parses the number of channels from the value attribute of an AudioElementConfiguration with * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5 From a1999ef854d52e65ceaf25915a2f759580cd4c32 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 24 Sep 2020 14:51:02 +0100 Subject: [PATCH 21/69] Util.getStringForTime() prefixes negative times Fix bug to place the negative sign in the beginning of the returned String. PiperOrigin-RevId: 333504868 --- .../java/com/google/android/exoplayer2/util/Util.java | 8 ++++++-- .../java/com/google/android/exoplayer2/util/UtilTest.java | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) 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 f954b60c453..5505649cf0e 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 @@ -17,6 +17,7 @@ import static android.content.Context.UI_MODE_SERVICE; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static java.lang.Math.abs; import static java.lang.Math.max; import static java.lang.Math.min; @@ -1844,13 +1845,16 @@ public static String getStringForTime(StringBuilder builder, Formatter formatter if (timeMs == C.TIME_UNSET) { timeMs = 0; } + String prefix = timeMs < 0 ? "-" : ""; + timeMs = abs(timeMs); long totalSeconds = (timeMs + 500) / 1000; long seconds = totalSeconds % 60; long minutes = (totalSeconds / 60) % 60; long hours = totalSeconds / 3600; builder.setLength(0); - return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - : formatter.format("%02d:%02d", minutes, seconds).toString(); + return hours > 0 + ? formatter.format("%s%d:%02d:%02d", prefix, hours, minutes, seconds).toString() + : formatter.format("%s%02d:%02d", prefix, minutes, seconds).toString(); } /** 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 162dcbae9d0..cda9e054f16 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 @@ -19,6 +19,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.parseXsDateTime; import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.unescapeFileName; @@ -37,6 +38,7 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; +import java.util.Formatter; import java.util.Random; import java.util.zip.Deflater; import org.junit.Test; @@ -1082,6 +1084,12 @@ public void tableExists_withNonExistingTable() { assertThat(Util.tableExists(database, "table")).isFalse(); } + @Test + public void getStringForTime_withNegativeTime_setsNegativePrefix() { + assertThat(getStringForTime(new StringBuilder(), new Formatter(), /* timeMs= */ -35000)) + .isEqualTo("-00:35"); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); From 973d23543ef97bdaa75b1876381b4155f07beb6e Mon Sep 17 00:00:00 2001 From: samrobinson Date: Fri, 25 Sep 2020 12:36:10 +0100 Subject: [PATCH 22/69] Add support for mp2 boxes. Issue: #7967 PiperOrigin-RevId: 333709003 --- RELEASENOTES.md | 3 +++ .../java/com/google/android/exoplayer2/extractor/mp4/Atom.java | 3 +++ .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dbeb77236ac..9353d1e73ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). +* Extractors: + * Add support for .mp2 boxes in the `AtomParsers` + ([#7967](https://github.com/google/ExoPlayer/issues/7967)). ### 2.12.0 (2020-09-11) ### diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 58f3a75b874..325dc24aeca 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -115,6 +115,9 @@ @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_mp4a = 0x6d703461; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE__mp2 = 0x2e6d7032; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE__mp3 = 0x2e6d7033; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 0ab126367b0..573451ef6a2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -878,6 +878,7 @@ private static StsdData parseStsd( || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt || childAtomType == Atom.TYPE_twos + || childAtomType == Atom.TYPE__mp2 || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -1243,7 +1244,7 @@ private static void parseAudioSampleEntry( } else if (atomType == Atom.TYPE_twos) { mimeType = MimeTypes.AUDIO_RAW; pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; - } else if (atomType == Atom.TYPE__mp3) { + } else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { mimeType = MimeTypes.AUDIO_ALAC; From 300bee5f0b727d470c8a705cfdd3e245d5ca4fcc Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 25 Sep 2020 13:10:35 +0100 Subject: [PATCH 23/69] Bring back setRenderTimeLimitMs PiperOrigin-RevId: 333712782 --- .../mediacodec/MediaCodecRenderer.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index de3f595976f..a2ba72dbc0e 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 @@ -363,6 +363,7 @@ private static String buildCustomDiagnosticInfo(int errorCode) { @Nullable private DrmSession sourceDrmSession; @Nullable private MediaCrypto mediaCrypto; private boolean mediaCryptoRequiresSecureDecoder; + private long renderTimeLimitMs; private float operatingRate; @Nullable private MediaCodec codec; @Nullable private MediaCodecAdapter codecAdapter; @@ -442,6 +443,7 @@ public MediaCodecRenderer( outputBufferInfo = new MediaCodec.BufferInfo(); operatingRate = 1f; mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS; + renderTimeLimitMs = C.TIME_UNSET; pendingOutputStreamStartPositionsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; @@ -451,6 +453,19 @@ public MediaCodecRenderer( resetCodecStateForRelease(); } + /** + * Set a limit on the time a single {@link #render(long, long)} call can spend draining and + * filling the decoder. + * + *

    This method should be called right after creating an instance of this class. + * + * @param renderTimeLimitMs The render time limit in milliseconds, or {@link C#TIME_UNSET} for no + * limit. + */ + public void setRenderTimeLimitMs(long renderTimeLimitMs) { + this.renderTimeLimitMs = renderTimeLimitMs; + } + /** * Set the mode of operation of the underlying {@link MediaCodec}. * @@ -837,9 +852,11 @@ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackEx while (bypassRender(positionUs, elapsedRealtimeUs)) {} TraceUtil.endSection(); } else if (codec != null) { + long renderStartTimeMs = SystemClock.elapsedRealtime(); TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer()) {} + while (drainOutputBuffer(positionUs, elapsedRealtimeUs) + && shouldContinueRendering(renderStartTimeMs)) {} + while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {} TraceUtil.endSection(); } else { decoderCounters.skippedInputBufferCount += skipSource(positionUs); @@ -1171,6 +1188,11 @@ private void initCodec(MediaCodecInfo codecInfo, MediaCrypto crypto) throws Exce onCodecInitialized(codecName, codecInitializedTimestamp, elapsed); } + private boolean shouldContinueRendering(long renderStartTimeMs) { + return renderTimeLimitMs == C.TIME_UNSET + || SystemClock.elapsedRealtime() - renderStartTimeMs < renderTimeLimitMs; + } + private void getCodecBuffers(MediaCodec codec) { if (Util.SDK_INT < 21) { inputBuffers = codec.getInputBuffers(); From 19530866c72801d96e929164e957b3677e76999e Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 25 Sep 2020 15:08:08 +0100 Subject: [PATCH 24/69] Always pass true for ongoing with the first notification ISSUE: #7977 PiperOrigin-RevId: 333726625 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index e23c91cd16c..b52a3e6f82b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -989,7 +989,6 @@ private void startOrUpdateNotification(Player player, @Nullable Bitmap bitmap) { Notification notification = builder.build(); notificationManager.notify(notificationId, notification); if (!isNotificationStarted) { - isNotificationStarted = true; context.registerReceiver(notificationBroadcastReceiver, intentFilter); if (notificationListener != null) { notificationListener.onNotificationStarted(notificationId, notification); @@ -997,8 +996,12 @@ private void startOrUpdateNotification(Player player, @Nullable Bitmap bitmap) { } @Nullable NotificationListener listener = notificationListener; if (listener != null) { - listener.onNotificationPosted(notificationId, notification, ongoing); + // Always pass true for ongoing with the first notification to tell a service to go into + // foreground even when paused. + listener.onNotificationPosted( + notificationId, notification, ongoing || !isNotificationStarted); } + isNotificationStarted = true; } // We're calling a deprecated listener method that we still want to notify. From 57f11d155613ddb9c9f59c8c6f0b78f7d55ca1f6 Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 25 Sep 2020 16:01:54 +0100 Subject: [PATCH 25/69] Parse TLEN duration in Mp3Extractor Issue: #7949 PiperOrigin-RevId: 333733615 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/mp3/MlltSeeker.java | 17 +++++++++------ .../extractor/mp3/Mp3Extractor.java | 21 +++++++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9353d1e73ac..c0c78f9ac28 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,8 @@ * Extractors: * Add support for .mp2 boxes in the `AtomParsers` ([#7967](https://github.com/google/ExoPlayer/issues/7967)). + * Use TLEN ID3 tag to compute the duration in Mp3Extractor + ([#7949](https://github.com/google/ExoPlayer/issues/7949)). ### 2.12.0 (2020-09-11) ### diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java index 1b627483f08..f30b8302497 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java @@ -29,9 +29,11 @@ * * @param firstFramePosition The position of the start of the first frame in the stream. * @param mlltFrame The MLLT frame with seeking metadata. + * @param durationUs The stream duration in microseconds, or {@link C#TIME_UNSET} if it is + * unknown. * @return An {@link MlltSeeker} for seeking in the stream. */ - public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) { + public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame, long durationUs) { int referenceCount = mlltFrame.bytesDeviations.length; long[] referencePositions = new long[1 + referenceCount]; long[] referenceTimesMs = new long[1 + referenceCount]; @@ -45,19 +47,22 @@ public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) { referencePositions[i] = position; referenceTimesMs[i] = timeMs; } - return new MlltSeeker(referencePositions, referenceTimesMs); + return new MlltSeeker(referencePositions, referenceTimesMs, durationUs); } private final long[] referencePositions; private final long[] referenceTimesMs; private final long durationUs; - private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) { + private MlltSeeker(long[] referencePositions, long[] referenceTimesMs, long durationUs) { this.referencePositions = referencePositions; this.referenceTimesMs = referenceTimesMs; - // Use the last reference point as the duration, as extrapolating variable bitrate at the end of - // the stream may give a large error. - durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]); + // Use the last reference point as the duration if it is unknown, as extrapolating variable + // bitrate at the end of the stream may give a large error. + this.durationUs = + durationUs != C.TIME_UNSET + ? durationUs + : C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]); } @Override diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 59d128ab9b7..c2aba6d7bd6 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -432,7 +433,7 @@ private Seeker computeSeeker(ExtractorInput input) throws IOException { @Nullable Seeker resultSeeker = null; if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) { - long durationUs = C.TIME_UNSET; + long durationUs; long dataEndPosition = C.POSITION_UNSET; if (metadataSeeker != null) { durationUs = metadataSeeker.getDurationUs(); @@ -440,6 +441,8 @@ private Seeker computeSeeker(ExtractorInput input) throws IOException { } else if (seekFrameSeeker != null) { durationUs = seekFrameSeeker.getDurationUs(); dataEndPosition = seekFrameSeeker.getDataEndPosition(); + } else { + durationUs = getId3TlenUs(metadata); } resultSeeker = new IndexSeeker( @@ -554,10 +557,24 @@ private static MlltSeeker maybeHandleSeekMetadata( for (int i = 0; i < length; i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof MlltFrame) { - return MlltSeeker.create(firstFramePosition, (MlltFrame) entry); + return MlltSeeker.create(firstFramePosition, (MlltFrame) entry, getId3TlenUs(metadata)); } } } return null; } + + private static long getId3TlenUs(@Nullable Metadata metadata) { + if (metadata != null) { + int length = metadata.length(); + for (int i = 0; i < length; i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof TextInformationFrame + && ((TextInformationFrame) entry).id.equals("TLEN")) { + return C.msToUs(Long.parseLong(((TextInformationFrame) entry).value)); + } + } + } + return C.TIME_UNSET; + } } From 9819664bd15ab221bb268c8d6273e2d8638d699c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 25 Sep 2020 17:49:12 +0100 Subject: [PATCH 26/69] Merge pull request #7798 from yoobi:trackSelectionView PiperOrigin-RevId: 333751261 --- RELEASENOTES.md | 4 ++ .../exoplayer2/demo/TrackSelectionDialog.java | 7 ++- .../ui/TrackSelectionDialogBuilder.java | 21 +++++++- .../exoplayer2/ui/TrackSelectionView.java | 50 ++++++++++++++++--- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c0c78f9ac28..8b95794e933 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,10 @@ ([#7967](https://github.com/google/ExoPlayer/issues/7967)). * Use TLEN ID3 tag to compute the duration in Mp3Extractor ([#7949](https://github.com/google/ExoPlayer/issues/7949)). +* UI + * Add the option to sort tracks by `Format` in `TrackSelectionView` and + `TrackSelectionDialogBuilder` + ([#7709](https://github.com/google/ExoPlayer/issues/7709)). ### 2.12.0 (2020-09-11) ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java index 5cf2353f21a..d3f9b3880da 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -354,7 +354,12 @@ public View onCreateView( trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.init( - mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); + mappedTrackInfo, + rendererIndex, + isDisabled, + overrides, + /* trackFormatComparator= */ null, + /* listener= */ this); return rootView; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java index 520b2d7580c..be3fb9bc90f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java @@ -25,6 +25,7 @@ import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; @@ -32,6 +33,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionUtil; import java.lang.reflect.Constructor; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** Builder for a dialog with a {@link TrackSelectionView}. */ @@ -62,6 +64,7 @@ public interface DialogCallback { @Nullable private TrackNameProvider trackNameProvider; private boolean isDisabled; private List overrides; + @Nullable private Comparator trackFormatComparator; /** * Creates a builder for a track selection dialog. @@ -208,6 +211,16 @@ public TrackSelectionDialogBuilder setShowDisableOption(boolean showDisableOptio return this; } + /** + * Sets a {@link Comparator} used to determine the display order of the tracks within each track + * group. + * + * @param trackFormatComparator The comparator, or {@code null} to use the original order. + */ + public void setTrackFormatComparator(@Nullable Comparator trackFormatComparator) { + this.trackFormatComparator = trackFormatComparator; + } + /** * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and * updates the view with track names queried from the specified provider. @@ -287,7 +300,13 @@ private Dialog.OnClickListener setUpDialogView(View dialogView) { if (trackNameProvider != null) { selectionView.setTrackNameProvider(trackNameProvider); } - selectionView.init(mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ null); + selectionView.init( + mappedTrackInfo, + rendererIndex, + isDisabled, + overrides, + trackFormatComparator, + /* listener= */ null); return (dialog, which) -> callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides()); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index b47feb2a718..8a8f3d3c76f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -18,7 +18,6 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import android.util.Pair; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -26,6 +25,7 @@ import android.widget.LinearLayout; import androidx.annotation.AttrRes; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -71,6 +72,7 @@ public interface TrackSelectionListener { private int rendererIndex; private TrackGroupArray trackGroups; private boolean isDisabled; + @Nullable private Comparator trackInfoComparator; @Nullable private TrackSelectionListener listener; /** Creates a track selection view. */ @@ -196,6 +198,8 @@ public void setTrackNameProvider(TrackNameProvider trackNameProvider) { * @param overrides List of initial overrides to be shown for this renderer. There must be at most * one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't * been set to {@code true}, only the first override is used. + * @param trackFormatComparator An optional comparator used to determine the display order of the + * tracks within each track group. * @param listener An optional listener for track selection updates. */ public void init( @@ -203,10 +207,15 @@ public void init( int rendererIndex, boolean isDisabled, List overrides, + @Nullable Comparator trackFormatComparator, @Nullable TrackSelectionListener listener) { this.mappedTrackInfo = mappedTrackInfo; this.rendererIndex = rendererIndex; this.isDisabled = isDisabled; + this.trackInfoComparator = + trackFormatComparator == null + ? null + : (o1, o2) -> trackFormatComparator.compare(o1.format, o2.format); this.listener = listener; int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1); for (int i = 0; i < maxOverrides; i++) { @@ -259,7 +268,16 @@ private void updateViews() { TrackGroup group = trackGroups.get(groupIndex); boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex); trackViews[groupIndex] = new CheckedTextView[group.length]; + + TrackInfo[] trackInfos = new TrackInfo[group.length]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + trackInfos[trackIndex] = new TrackInfo(groupIndex, trackIndex, group.getFormat(trackIndex)); + } + if (trackInfoComparator != null) { + Arrays.sort(trackInfos, trackInfoComparator); + } + + for (int trackIndex = 0; trackIndex < trackInfos.length; trackIndex++) { if (trackIndex == 0) { addView(inflater.inflate(R.layout.exo_list_divider, this, false)); } @@ -270,11 +288,11 @@ private void updateViews() { CheckedTextView trackView = (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex))); + trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format)); if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED) { trackView.setFocusable(true); - trackView.setTag(Pair.create(groupIndex, trackIndex)); + trackView.setTag(trackInfos[trackIndex]); trackView.setOnClickListener(componentListener); } else { trackView.setFocusable(false); @@ -294,7 +312,12 @@ private void updateViewStates() { for (int i = 0; i < trackViews.length; i++) { SelectionOverride override = overrides.get(i); for (int j = 0; j < trackViews[i].length; j++) { - trackViews[i][j].setChecked(override != null && override.containsTrack(j)); + if (override != null) { + TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(trackViews[i][j].getTag()); + trackViews[i][j].setChecked(override.containsTrack(trackInfo.trackIndex)); + } else { + trackViews[i][j].setChecked(false); + } } } } @@ -325,10 +348,9 @@ private void onDefaultViewClicked() { private void onTrackViewClicked(View view) { isDisabled = false; - @SuppressWarnings("unchecked") - Pair tag = (Pair) Assertions.checkNotNull(view.getTag()); - int groupIndex = tag.first; - int trackIndex = tag.second; + TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(view.getTag()); + int groupIndex = trackInfo.groupIndex; + int trackIndex = trackInfo.trackIndex; SelectionOverride override = overrides.get(groupIndex); Assertions.checkNotNull(mappedTrackInfo); if (override == null) { @@ -406,4 +428,16 @@ public void onClick(View view) { TrackSelectionView.this.onClick(view); } } + + private static final class TrackInfo { + public final int groupIndex; + public final int trackIndex; + public final Format format; + + public TrackInfo(int groupIndex, int trackIndex, Format format) { + this.groupIndex = groupIndex; + this.trackIndex = trackIndex; + this.format = format; + } + } } From 1bdccd4bfb3d9d23543c138445215beb6d190856 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Sep 2020 10:27:18 +0100 Subject: [PATCH 27/69] Fix position reporting with fetch errors On receiving a fetch error for an ad that would otherwise play based on an initial/seek position, the pending content position wasn't cleared which meant that position reporting was broken after a fetch error. Fix this by always clearing the pending position (if there was a pending position that will have triggered the fetch error). Also deduplicate the code for handling empty ad groups (fetch errors) and ad group load errors. Issue: #7956 PiperOrigin-RevId: 334113131 --- RELEASENOTES.md | 3 ++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 41 ++++++------------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 26 ++++++++++++ 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b95794e933..8ffa7562e8f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,9 @@ * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). +* IMA extension: + * Fix position reporting after fetch errors + ([#7956](https://github.com/google/ExoPlayer/issues/7956)). ### 2.12.0 (2020-09-11) ### 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 88b0daac493..cf8d487ede0 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 @@ -1077,7 +1077,7 @@ private void handleAdEvent(AdEvent adEvent) { adGroupTimeSeconds == -1.0 ? adPlaybackState.adGroupCount - 1 : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds); - handleAdGroupFetchError(adGroupIndex); + markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); break; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads @@ -1364,35 +1364,20 @@ private void stopAdInternal(AdMediaInfo adMediaInfo) { } } - private void handleAdGroupFetchError(int adGroupIndex) { - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (DEBUG) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - } - private void handleAdGroupLoadError(Exception error) { - if (player == null) { - return; - } - - // TODO: Once IMA signals which ad group failed to load, remove this call. int adGroupIndex = getLoadingAdGroupIndex(); if (adGroupIndex == C.INDEX_UNSET) { Log.w(TAG, "Unable to determine ad group index for ad group load error", error); return; } + markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); + if (pendingAdLoadError == null) { + pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); + } + } + private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) { + // Update the ad playback state so all ads in the ad group are in the error state. AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); @@ -1407,9 +1392,7 @@ private void handleAdGroupLoadError(Exception error) { } } updateAdPlaybackState(); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } + // Clear any pending content position that triggered attempting to load the ad group. pendingContentPositionMs = C.TIME_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; } @@ -1522,8 +1505,10 @@ private int getAdGroupIndexForAdPod(AdPodInfo adPodInfo) { * no such ad group. */ private int getLoadingAdGroupIndex() { - long playerPositionUs = - C.msToUs(getContentPeriodPositionMs(checkNotNull(player), timeline, period)); + if (player == null) { + return C.INDEX_UNSET; + } + long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period)); int adGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs)); if (adGroupIndex == C.INDEX_UNSET) { 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 e32a1992006..c2cc3848886 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 @@ -48,6 +48,7 @@ import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.MediaItem; @@ -311,6 +312,31 @@ public void playback_withMidrollFetchError_marksAdAsInErrorState() { .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); } + @Test + public void playback_withMidrollFetchError_updatesContentProgress() { + AdEvent mockMidrollFetchErrorAdEvent = mock(AdEvent.class); + when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); + when(mockMidrollFetchErrorAdEvent.getAdData()) + .thenReturn(ImmutableMap.of("adBreakTime", "5.5")); + setupPlayback(CONTENT_TIMELINE, ImmutableList.of(5.5f)); + + // Simulate loading an empty midroll ad and advancing the player position. + imaAdsLoader.start(adsLoaderListener, adViewProvider); + adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent); + long playerPositionUs = CONTENT_DURATION_US - C.MICROS_PER_SECOND; + long playerPositionInPeriodUs = + playerPositionUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; + long periodDurationUs = + CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; + fakeExoPlayer.setPlayingContentPosition(C.usToMs(playerPositionUs)); + + // Verify the content progress is updated to reflect the new player position. + assertThat(contentProgressProvider.getContentProgress()) + .isEqualTo( + new VideoProgressUpdate( + C.usToMs(playerPositionInPeriodUs), C.usToMs(periodDurationUs))); + } + @Test public void playback_withPostrollFetchError_marksAdAsInErrorState() { AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class); From 89cd796bf68fd72aa20e5c46f4d1d85fcaf13f52 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Mon, 28 Sep 2020 11:45:00 +0100 Subject: [PATCH 28/69] Do not require subtitleButton in custom layouts of StyledPlayerView Every other subtitleButton has an if not null check, but does not force non null. Issue: #7962 PiperOrigin-RevId: 334124323 --- RELEASENOTES.md | 3 +++ .../exoplayer2/ui/StyledPlayerControlView.java | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ffa7562e8f..e7f88cba51f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). +* UI: + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Extractors: * Add support for .mp2 boxes in the `AtomParsers` ([#7967](https://github.com/google/ExoPlayer/issues/7967)). 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 8bb9babeb0b..c3add8f8af9 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 @@ -2005,11 +2005,13 @@ public void init( break; } } - checkNotNull(subtitleButton) - .setImageDrawable(subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable); - checkNotNull(subtitleButton) - .setContentDescription( - subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription); + + if (subtitleButton != null) { + subtitleButton.setImageDrawable( + subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable); + subtitleButton.setContentDescription( + subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription); + } this.rendererIndices = rendererIndices; this.tracks = trackInfos; this.mappedTrackInfo = mappedTrackInfo; From 824b2a7305bd2b551c2dea6d036cb61fabe6c3ad Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Sep 2020 15:15:26 +0100 Subject: [PATCH 29/69] Fix position ramping behavior with AudioTrack speed params Non-realtime AudioTrack playback speed was not taken into account when extrapolating the old mode's position, causing the position not to advance smoothly. This should be a no-op when not using AudioTrack playback params for speed adjustment. Issue: #7982 PiperOrigin-RevId: 334151163 --- RELEASENOTES.md | 4 ++++ .../android/exoplayer2/audio/AudioTrackPositionTracker.java | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e7f88cba51f..55aaa5d486e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,10 @@ * UI: * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). +* Audio: + * Fix the default audio sink position not advancing correctly when using + `AudioTrack`-based speed adjustment + ([#7982](https://github.com/google/ExoPlayer/issues/7982)). * Extractors: * Add support for .mp2 boxes in the `AtomParsers` ([#7967](https://github.com/google/ExoPlayer/issues/7967)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index 540ee098ee6..8891a6d8d1e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -289,7 +289,10 @@ public long getCurrentPositionUs(boolean sourceEnded) { if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) { // Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden // jump if the two modes disagree. - long previousModeProjectedPositionUs = previousModePositionUs + elapsedSincePreviousModeUs; + long previousModeProjectedPositionUs = + previousModePositionUs + + Util.getMediaDurationForPlayoutDuration( + elapsedSincePreviousModeUs, audioTrackPlaybackSpeed); // A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US. long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US; positionUs *= rampPoint; From 358d205f0fea9879ee6db092328b070274de7776 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Sep 2020 11:08:04 +0100 Subject: [PATCH 30/69] Use Builder in ImaAdsLoader constructor PiperOrigin-RevId: 334562209 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 94 ++++--------------- 1 file changed, 18 insertions(+), 76 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index cf8d487ede0..157fab938c6 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 @@ -142,7 +142,7 @@ public static final class Builder { * @param context The context; */ public Builder(Context context) { - this.context = checkNotNull(context); + this.context = checkNotNull(context).getApplicationContext(); adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS; vastLoadTimeoutMs = TIMEOUT_UNSET; mediaLoadTimeoutMs = TIMEOUT_UNSET; @@ -318,21 +318,7 @@ public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) { */ public ImaAdsLoader buildForAdTag(Uri adTagUri) { return new ImaAdsLoader( - context, - adTagUri, - imaSdkSettings, - /* adsResponse= */ null, - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - imaFactory); + /* builder= */ this, /* adTagUri= */ adTagUri, /* adsResponse= */ null); } /** @@ -343,22 +329,7 @@ public ImaAdsLoader buildForAdTag(Uri adTagUri) { * @return The new {@link ImaAdsLoader}. */ public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader( - context, - /* adTagUri= */ null, - imaSdkSettings, - adsResponse, - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - imaFactory); + return new ImaAdsLoader(/* builder= */ this, /* adTagUri= */ null, adsResponse); } } @@ -520,56 +491,27 @@ public ImaAdsLoader buildForAdsResponse(String adsResponse) { * more information. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this( - context, - adTagUri, - /* imaSdkSettings= */ null, - /* adsResponse= */ null, - /* adPreloadTimeoutMs= */ Builder.DEFAULT_AD_PRELOAD_TIMEOUT_MS, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* playAdBeforeStartPosition= */ true, - /* adUiElements= */ null, - /* companionAdSlots= */ null, - /* adErrorListener= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); + this(new Builder(context), adTagUri, /* adsResponse= */ null); } @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"}) - private ImaAdsLoader( - Context context, - @Nullable Uri adTagUri, - @Nullable ImaSdkSettings imaSdkSettings, - @Nullable String adsResponse, - long adPreloadTimeoutMs, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - int mediaBitrate, - boolean focusSkipButtonWhenAvailable, - boolean playAdBeforeStartPosition, - @Nullable Set adUiElements, - @Nullable Collection companionAdSlots, - @Nullable AdErrorListener adErrorListener, - @Nullable AdEventListener adEventListener, - ImaFactory imaFactory) { + private ImaAdsLoader(Builder builder, @Nullable Uri adTagUri, @Nullable String adsResponse) { checkArgument(adTagUri != null || adsResponse != null); - this.context = context.getApplicationContext(); + this.context = builder.context.getApplicationContext(); this.adTagUri = adTagUri; this.adsResponse = adsResponse; - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.mediaBitrate = mediaBitrate; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - this.adUiElements = adUiElements; - this.companionAdSlots = companionAdSlots; - this.adErrorListener = adErrorListener; - this.adEventListener = adEventListener; - this.imaFactory = imaFactory; + this.adPreloadTimeoutMs = builder.adPreloadTimeoutMs; + this.vastLoadTimeoutMs = builder.vastLoadTimeoutMs; + this.mediaLoadTimeoutMs = builder.mediaLoadTimeoutMs; + this.mediaBitrate = builder.mediaBitrate; + this.focusSkipButtonWhenAvailable = builder.focusSkipButtonWhenAvailable; + this.playAdBeforeStartPosition = builder.playAdBeforeStartPosition; + this.adUiElements = builder.adUiElements; + this.companionAdSlots = builder.companionAdSlots; + this.adErrorListener = builder.adErrorListener; + this.adEventListener = builder.adEventListener; + this.imaFactory = builder.imaFactory; + @Nullable ImaSdkSettings imaSdkSettings = builder.imaSdkSettings; if (imaSdkSettings == null) { imaSdkSettings = imaFactory.createImaSdkSettings(); if (DEBUG) { From f3767b3185202338725274a37b4ce0c3080ac417 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Sep 2020 11:54:01 +0100 Subject: [PATCH 31/69] Add ImaUtil for IMA extension utilities PiperOrigin-RevId: 334567234 --- .../ext/ima/AdPlaybackStateFactory.java | 56 -------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 77 ++--------- .../android/exoplayer2/ext/ima/ImaUtil.java | 128 ++++++++++++++++++ .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 28 ++-- 4 files changed, 155 insertions(+), 134 deletions(-) delete mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java deleted file mode 100644 index a97307a4195..00000000000 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.ima; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import java.util.Arrays; -import java.util.List; - -/** - * Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data. - */ -/* package */ final class AdPlaybackStateFactory { - private AdPlaybackStateFactory() {} - - /** - * Construct an {@link AdPlaybackState} from the provided {@code cuePoints}. - * - * @param cuePoints The cue points of the ads in seconds. - * @return The {@link AdPlaybackState}. - */ - public static AdPlaybackState fromCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new AdPlaybackState(/* adGroupTimesUs...= */ 0); - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return new AdPlaybackState(adGroupTimesUs); - } -} 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 157fab938c6..592920bfc48 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 @@ -33,7 +33,6 @@ import androidx.annotation.VisibleForTesting; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; import com.google.ads.interactivemedia.v3.api.AdEvent; @@ -134,7 +133,7 @@ public static final class Builder { private int mediaBitrate; private boolean focusSkipButtonWhenAvailable; private boolean playAdBeforeStartPosition; - private ImaFactory imaFactory; + private ImaUtil.ImaFactory imaFactory; /** * Creates a new builder for {@link ImaAdsLoader}. @@ -303,7 +302,7 @@ public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) { } @VisibleForTesting - /* package */ Builder setImaFactory(ImaFactory imaFactory) { + /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) { this.imaFactory = checkNotNull(imaFactory); return this; } @@ -397,7 +396,7 @@ public ImaAdsLoader buildForAdsResponse(String adsResponse) { @Nullable private final Collection companionAdSlots; @Nullable private final AdErrorListener adErrorListener; @Nullable private final AdEventListener adEventListener; - private final ImaFactory imaFactory; + private final ImaUtil.ImaFactory imaFactory; private final ImaSdkSettings imaSdkSettings; private final Timeline.Period period; private final Handler handler; @@ -677,7 +676,7 @@ public void start(EventListener eventListener, AdViewProvider adViewProvider) { adsManager.resume(); } } else if (adsManager != null) { - adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); + adPlaybackState = ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); updateAdPlaybackState(); } else { // Ads haven't loaded yet, so request them. @@ -688,7 +687,7 @@ public void start(EventListener eventListener, AdViewProvider adViewProvider) { adDisplayContainer.registerFriendlyObstruction( imaFactory.createFriendlyObstruction( overlayInfo.view, - getFriendlyObstructionPurpose(overlayInfo.purpose), + ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), overlayInfo.reasonDetail)); } } @@ -1481,21 +1480,6 @@ private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; } - private static FriendlyObstructionPurpose getFriendlyObstructionPurpose( - @OverlayInfo.Purpose int purpose) { - switch (purpose) { - case OverlayInfo.PURPOSE_CONTROLS: - return FriendlyObstructionPurpose.VIDEO_CONTROLS; - case OverlayInfo.PURPOSE_CLOSE_AD: - return FriendlyObstructionPurpose.CLOSE_AD; - case OverlayInfo.PURPOSE_NOT_VISIBLE: - return FriendlyObstructionPurpose.NOT_VISIBLE; - case OverlayInfo.PURPOSE_OTHER: - default: - return FriendlyObstructionPurpose.OTHER; - } - } - private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); } @@ -1509,13 +1493,6 @@ private static long getContentPeriodPositionMs( : timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs()); } - private static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; - } - private static Looper getImaLooper() { // IMA SDK callbacks occur on the main thread. This method can be used to check that the player // is using the same looper, to ensure all interaction with this class is on the main thread. @@ -1549,38 +1526,6 @@ private void destroyAdsManager() { } } - /** Factory for objects provided by the IMA SDK. */ - @VisibleForTesting - /* package */ interface ImaFactory { - /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ - ImaSdkSettings createImaSdkSettings(); - /** - * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that - * control rendering of ads. - */ - AdsRenderingSettings createAdsRenderingSettings(); - /** - * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for - * non-linear ads, and slots for companion ads. - */ - AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); - /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ - AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); - /** - * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for - * viewability measurement purposes. - */ - FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail); - /** Creates an {@link AdsRequest} to contain the data used to request ads. */ - AdsRequest createAdsRequest(); - /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ - AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - private final class ComponentListener implements AdsLoadedListener, ContentProgressProvider, @@ -1610,7 +1555,8 @@ public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { if (player != null) { // If a player is attached already, start playback immediately. try { - adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); + adPlaybackState = + ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); hasAdPlaybackState = true; updateAdPlaybackState(); } catch (RuntimeException e) { @@ -1680,7 +1626,7 @@ public void onAdError(AdErrorEvent adErrorEvent) { adPlaybackState = AdPlaybackState.NONE; hasAdPlaybackState = true; updateAdPlaybackState(); - } else if (isAdGroupLoadError(error)) { + } else if (ImaUtil.isAdGroupLoadError(error)) { try { handleAdGroupLoadError(error); } catch (RuntimeException e) { @@ -1795,8 +1741,11 @@ public String toString() { } } - /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ - private static final class DefaultImaFactory implements ImaFactory { + /** + * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link + * ImaSdkFactory}. + */ + private static final class DefaultImaFactory implements ImaUtil.ImaFactory { @Override public ImaSdkSettings createImaSdkSettings() { return ImaSdkFactory.getInstance().createImaSdkSettings(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java new file mode 100644 index 00000000000..c4b2c3dca37 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -0,0 +1,128 @@ +/* + * 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.ima; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdError; +import com.google.ads.interactivemedia.v3.api.AdsLoader; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; +import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; +import java.util.Arrays; +import java.util.List; + +/** Utilities for working with IMA SDK and IMA extension data types. */ +/* package */ final class ImaUtil { + + /** Factory for objects provided by the IMA SDK. */ + public interface ImaFactory { + /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ + ImaSdkSettings createImaSdkSettings(); + /** + * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that + * control rendering of ads. + */ + AdsRenderingSettings createAdsRenderingSettings(); + /** + * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for + * non-linear ads, and slots for companion ads. + */ + AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); + /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ + AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); + /** + * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for + * viewability measurement purposes. + */ + FriendlyObstruction createFriendlyObstruction( + View view, + FriendlyObstructionPurpose friendlyObstructionPurpose, + @Nullable String reasonDetail); + /** Creates an {@link AdsRequest} to contain the data used to request ads. */ + AdsRequest createAdsRequest(); + /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ + AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); + } + + /** + * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link + * OverlayInfo#purpose}. + */ + public static FriendlyObstructionPurpose getFriendlyObstructionPurpose( + @OverlayInfo.Purpose int purpose) { + switch (purpose) { + case OverlayInfo.PURPOSE_CONTROLS: + return FriendlyObstructionPurpose.VIDEO_CONTROLS; + case OverlayInfo.PURPOSE_CLOSE_AD: + return FriendlyObstructionPurpose.CLOSE_AD; + case OverlayInfo.PURPOSE_NOT_VISIBLE: + return FriendlyObstructionPurpose.NOT_VISIBLE; + case OverlayInfo.PURPOSE_OTHER: + default: + return FriendlyObstructionPurpose.OTHER; + } + } + + /** + * Returns an initial {@link AdPlaybackState} with ad groups at the provided {@code cuePoints}. + * + * @param cuePoints The cue points of the ads in seconds. + * @return The {@link AdPlaybackState}. + */ + public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List cuePoints) { + if (cuePoints.isEmpty()) { + // If no cue points are specified, there is a preroll ad. + return new AdPlaybackState(/* adGroupTimesUs...= */ 0); + } + + int count = cuePoints.size(); + long[] adGroupTimesUs = new long[count]; + int adGroupIndex = 0; + for (int i = 0; i < count; i++) { + double cuePoint = cuePoints.get(i); + if (cuePoint == -1.0) { + adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; + } else { + adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); + } + } + // Cue points may be out of order, so sort them. + Arrays.sort(adGroupTimesUs, 0, adGroupIndex); + return new AdPlaybackState(adGroupTimesUs); + } + + /** Returns whether the ad error indicates that an entire ad group failed to load. */ + public static boolean isAdGroupLoadError(AdError adError) { + // TODO: Find out what other errors need to be handled (if any), and whether each one relates to + // a single ad, ad group or the whole timeline. + return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH + || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; + } + + private ImaUtil() {} +} 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 c2cc3848886..98610654540 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 @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory; +import com.google.android.exoplayer2.ext.ima.ImaUtil.ImaFactory; import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -378,7 +378,7 @@ public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -402,7 +402,7 @@ public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) @@ -424,7 +424,7 @@ public void resumePlaybackBeforeMidroll_playsPreroll() { verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -448,7 +448,7 @@ public void resumePlaybackAtMidroll_skipsPreroll() { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -473,7 +473,7 @@ public void resumePlaybackAfterMidroll_skipsPreroll() { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -500,7 +500,7 @@ public void resumePlaybackBeforeSecondMidroll_playsFirstMidroll() { verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -531,7 +531,7 @@ public void resumePlaybackAtSecondMidroll_skipsFirstMidroll() { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -563,7 +563,7 @@ public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPr .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -595,7 +595,7 @@ public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPrerol .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -622,7 +622,7 @@ public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMid verify(mockAdsManager).destroy(); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withSkippedAdGroup(/* adGroupIndex= */ 1)); @@ -663,7 +663,7 @@ public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMid .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -702,7 +702,7 @@ public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skips .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -761,7 +761,7 @@ public double getTimeOffset() { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI) From bd312ec906e983a9c444d7aa5e1a3b3f26e2bcd4 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 30 Sep 2020 16:15:31 +0100 Subject: [PATCH 32/69] Preserve limit when resetting ParsableByteArray in OggPacket#populate When I moved ParsableByteArray#data behind a getter I replaced some assignments with calls to reset(byte[]): https://github.com/google/ExoPlayer/commit/ce2e6e2fd625db787b1f400614adcd7458144bbd reset(byte[]) deliberately sets `limit` to `data.length`, in order to handle cases that were reassigning `data` but not updating `limit`. However OggPacket was already using `limit` to track where to write 'new' data into the array, so changing `limit` to `data.length` caused us to try and write new data beyond the end of the array. I looked at other uses of reset(byte[]) in https://github.com/google/ExoPlayer/commit/ce2e6e2fd625db787b1f400614adcd7458144bbd and condluded the only other usage in MatroskaExtractor is legit and shouldn't be updated like this (because MatroskaExtractor previously *wasn't* correctly updating/maintaining `limit`). Issue: #7992 PiperOrigin-RevId: 334601586 --- RELEASENOTES.md | 2 + .../exoplayer2/extractor/ogg/OggPacket.java | 7 +- .../ogg/OggExtractorParameterizedTest.java | 20 +- ...bear_vorbis_with_large_metadata.ogg.0.dump | 740 ++++++++++++++++++ ...bear_vorbis_with_large_metadata.ogg.1.dump | 456 +++++++++++ ...bear_vorbis_with_large_metadata.ogg.2.dump | 216 +++++ ...bear_vorbis_with_large_metadata.ogg.3.dump | 20 + ...ith_large_metadata.ogg.unknown_length.dump | 737 +++++++++++++++++ .../ogg/bear_vorbis_with_large_metadata.ogg | Bin 0 -> 111383 bytes 9 files changed, 2194 insertions(+), 4 deletions(-) create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump create mode 100644 testdata/src/test/assets/media/ogg/bear_vorbis_with_large_metadata.ogg diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55aaa5d486e..3404a3e7d96 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,8 @@ ([#7967](https://github.com/google/ExoPlayer/issues/7967)). * Use TLEN ID3 tag to compute the duration in Mp3Extractor ([#7949](https://github.com/google/ExoPlayer/issues/7949)). + * Fix regression for Ogg files with packets that span multiple pages + ([#7992](https://github.com/google/ExoPlayer/issues/7992)). * UI * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java index 450bff4a369..c7718e7fa9f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java @@ -88,7 +88,9 @@ public boolean populate(ExtractorInput input) throws IOException { int segmentIndex = currentSegmentIndex + segmentCount; if (size > 0) { if (packetArray.capacity() < packetArray.limit() + size) { - packetArray.reset(Arrays.copyOf(packetArray.getData(), packetArray.limit() + size)); + packetArray.reset( + Arrays.copyOf(packetArray.getData(), packetArray.limit() + size), + /* limit= */ packetArray.limit()); } input.readFully(packetArray.getData(), packetArray.limit(), size); packetArray.setLimit(packetArray.limit() + size); @@ -131,7 +133,8 @@ public void trimPayload() { } packetArray.reset( Arrays.copyOf( - packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit()))); + packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())), + /* limit= */ packetArray.limit()); } /** diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java index cc78d59bf48..0731cfd95e5 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java @@ -60,11 +60,27 @@ public void vorbis() throws Exception { OggExtractor::new, "media/ogg/bear_vorbis.ogg", simulationConfig); } - // Ensure the extractor can handle non-contiguous pages by using a file with 10 bytes of garbage - // data before the start of the second page. + /** + * Ensure the extractor can handle non-contiguous pages by using a file with 10 bytes of garbage + * data before the start of the second page. + * + *

    https://github.com/google/ExoPlayer/issues/7230 + */ @Test public void vorbisWithGapBeforeSecondPage() throws Exception { ExtractorAsserts.assertBehavior( OggExtractor::new, "media/ogg/bear_vorbis_gap.ogg", simulationConfig); } + + /** + * Use some very large Vorbis Comment metadata to create a packet that is larger than a single Ogg + * page. + * + *

    https://github.com/google/ExoPlayer/issues/7992 + */ + @Test + public void vorbisWithPacketSpanningBetweenPages() throws Exception { + ExtractorAsserts.assertBehavior( + OggExtractor::new, "media/ogg/bear_vorbis_with_large_metadata.ogg", simulationConfig); + } } diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump new file mode 100644 index 00000000000..92aec373b5e --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump @@ -0,0 +1,740 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 26873 + sample count = 180 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 0 + flags = 1 + data = length 49, hash 2FFF94F0 + sample 1: + time = 0 + flags = 1 + data = length 44, hash 3946418A + sample 2: + time = 2666 + flags = 1 + data = length 55, hash 2A0B878E + sample 3: + time = 5333 + flags = 1 + data = length 53, hash CC3B6879 + sample 4: + time = 8000 + flags = 1 + data = length 215, hash 106AE950 + sample 5: + time = 20000 + flags = 1 + data = length 192, hash 2B219F53 + sample 6: + time = 41333 + flags = 1 + data = length 197, hash FBC39422 + sample 7: + time = 62666 + flags = 1 + data = length 209, hash 386E8979 + sample 8: + time = 84000 + flags = 1 + data = length 42, hash E81162C1 + sample 9: + time = 96000 + flags = 1 + data = length 41, hash F15BEE36 + sample 10: + time = 98666 + flags = 1 + data = length 42, hash D67EB19 + sample 11: + time = 101333 + flags = 1 + data = length 42, hash F4DE4792 + sample 12: + time = 104000 + flags = 1 + data = length 53, hash 80F66AC3 + sample 13: + time = 106666 + flags = 1 + data = length 56, hash DCB9DFC4 + sample 14: + time = 109333 + flags = 1 + data = length 55, hash 4E0C4E9D + sample 15: + time = 112000 + flags = 1 + data = length 203, hash 176B6862 + sample 16: + time = 124000 + flags = 1 + data = length 193, hash AB13CB10 + sample 17: + time = 145333 + flags = 1 + data = length 203, hash DE63DE9F + sample 18: + time = 166666 + flags = 1 + data = length 194, hash 4A9508A2 + sample 19: + time = 188000 + flags = 1 + data = length 210, hash 196899B3 + sample 20: + time = 209333 + flags = 1 + data = length 195, hash B68407F1 + sample 21: + time = 230666 + flags = 1 + data = length 193, hash A1FA86E3 + sample 22: + time = 252000 + flags = 1 + data = length 194, hash 5C0B9343 + sample 23: + time = 273333 + flags = 1 + data = length 198, hash 789914B2 + sample 24: + time = 294666 + flags = 1 + data = length 183, hash 1B82D11F + sample 25: + time = 316000 + flags = 1 + data = length 199, hash D5B848F4 + sample 26: + time = 337333 + flags = 1 + data = length 192, hash B34427EA + sample 27: + time = 358666 + flags = 1 + data = length 199, hash C2599BB5 + sample 28: + time = 380000 + flags = 1 + data = length 195, hash BFD83194 + sample 29: + time = 401333 + flags = 1 + data = length 199, hash C9A7F7CA + sample 30: + time = 422666 + flags = 1 + data = length 44, hash 5D76EAD6 + sample 31: + time = 434666 + flags = 1 + data = length 43, hash 8619C423 + sample 32: + time = 437333 + flags = 1 + data = length 43, hash E490BBE + sample 33: + time = 440000 + flags = 1 + data = length 53, hash 8A557CAE + sample 34: + time = 442666 + flags = 1 + data = length 56, hash 81007BBA + sample 35: + time = 445333 + flags = 1 + data = length 56, hash 4E4DD67F + sample 36: + time = 448000 + flags = 1 + data = length 222, hash 414188AB + sample 37: + time = 460000 + flags = 1 + data = length 202, hash 67A07D30 + sample 38: + time = 481333 + flags = 1 + data = length 200, hash E357D853 + sample 39: + time = 502666 + flags = 1 + data = length 203, hash 4653DC90 + sample 40: + time = 524000 + flags = 1 + data = length 192, hash A65E6C09 + sample 41: + time = 545333 + flags = 1 + data = length 202, hash FBEAC508 + sample 42: + time = 566666 + flags = 1 + data = length 202, hash E9B7B59F + sample 43: + time = 588000 + flags = 1 + data = length 204, hash E24AA78E + sample 44: + time = 609333 + flags = 1 + data = length 41, hash 3FBC5216 + sample 45: + time = 621333 + flags = 1 + data = length 47, hash 153FBC55 + sample 46: + time = 624000 + flags = 1 + data = length 42, hash 2B493D6C + sample 47: + time = 626666 + flags = 1 + data = length 42, hash 8303BEE3 + sample 48: + time = 629333 + flags = 1 + data = length 62, hash 71AEE50B + sample 49: + time = 632000 + flags = 1 + data = length 54, hash 52F61908 + sample 50: + time = 634666 + flags = 1 + data = length 45, hash 7BD3E3A1 + sample 51: + time = 637333 + flags = 1 + data = length 41, hash E0F65472 + sample 52: + time = 640000 + flags = 1 + data = length 45, hash 41838675 + sample 53: + time = 642666 + flags = 1 + data = length 44, hash FCBC2147 + sample 54: + time = 645333 + flags = 1 + data = length 45, hash 1A5987E3 + sample 55: + time = 648000 + flags = 1 + data = length 43, hash 99074864 + sample 56: + time = 650666 + flags = 1 + data = length 57, hash D4A9B60A + sample 57: + time = 653333 + flags = 1 + data = length 52, hash 302129DA + sample 58: + time = 656000 + flags = 1 + data = length 57, hash D8DD99C0 + sample 59: + time = 658666 + flags = 1 + data = length 206, hash F4B9EF26 + sample 60: + time = 670666 + flags = 1 + data = length 197, hash 7B8ACC8A + sample 61: + time = 692000 + flags = 1 + data = length 186, hash 161027CB + sample 62: + time = 713333 + flags = 1 + data = length 186, hash 1D6871B6 + sample 63: + time = 734666 + flags = 1 + data = length 201, hash 536E9FDB + sample 64: + time = 756000 + flags = 1 + data = length 192, hash D38EFAC5 + sample 65: + time = 777333 + flags = 1 + data = length 194, hash 4B394EF3 + sample 66: + time = 798666 + flags = 1 + data = length 206, hash 1B31BA99 + sample 67: + time = 820000 + flags = 1 + data = length 212, hash AD061F43 + sample 68: + time = 841333 + flags = 1 + data = length 180, hash 6D1F7481 + sample 69: + time = 862666 + flags = 1 + data = length 195, hash D80B21F + sample 70: + time = 884000 + flags = 1 + data = length 186, hash D367882 + sample 71: + time = 905333 + flags = 1 + data = length 195, hash 2722159A + sample 72: + time = 926666 + flags = 1 + data = length 199, hash 10CEE97A + sample 73: + time = 948000 + flags = 1 + data = length 191, hash 2CF9FB3F + sample 74: + time = 969333 + flags = 1 + data = length 197, hash A725DA0 + sample 75: + time = 990666 + flags = 1 + data = length 211, hash D4E5DB9E + sample 76: + time = 1012000 + flags = 1 + data = length 189, hash 1A90F496 + sample 77: + time = 1033333 + flags = 1 + data = length 187, hash 44DB2689 + sample 78: + time = 1054666 + flags = 1 + data = length 197, hash 6D3E5117 + sample 79: + time = 1076000 + flags = 1 + data = length 208, hash 5B57B288 + sample 80: + time = 1097333 + flags = 1 + data = length 198, hash D5FC05 + sample 81: + time = 1118666 + flags = 1 + data = length 192, hash 350BBA45 + sample 82: + time = 1140000 + flags = 1 + data = length 195, hash 5F96F2A8 + sample 83: + time = 1161333 + flags = 1 + data = length 202, hash 61D7CC33 + sample 84: + time = 1182666 + flags = 1 + data = length 202, hash 49D335F2 + sample 85: + time = 1204000 + flags = 1 + data = length 192, hash 2FE9CB1A + sample 86: + time = 1225333 + flags = 1 + data = length 201, hash BF0763B2 + sample 87: + time = 1246666 + flags = 1 + data = length 184, hash AD047421 + sample 88: + time = 1268000 + flags = 1 + data = length 196, hash F9088F14 + sample 89: + time = 1289333 + flags = 1 + data = length 190, hash AC6D38FD + sample 90: + time = 1310666 + flags = 1 + data = length 195, hash 8D1A66D2 + sample 91: + time = 1332000 + flags = 1 + data = length 197, hash B46BFB6B + sample 92: + time = 1353333 + flags = 1 + data = length 195, hash D9761F23 + sample 93: + time = 1374666 + flags = 1 + data = length 204, hash 3391B617 + sample 94: + time = 1396000 + flags = 1 + data = length 42, hash 33A1FB52 + sample 95: + time = 1408000 + flags = 1 + data = length 44, hash 408B146E + sample 96: + time = 1410666 + flags = 1 + data = length 44, hash 171C7E0D + sample 97: + time = 1413333 + flags = 1 + data = length 54, hash 6307E16C + sample 98: + time = 1416000 + flags = 1 + data = length 53, hash 4A319572 + sample 99: + time = 1418666 + flags = 1 + data = length 215, hash BA9C445C + sample 100: + time = 1430666 + flags = 1 + data = length 201, hash 3120D234 + sample 101: + time = 1452000 + flags = 1 + data = length 187, hash DB44993C + sample 102: + time = 1473333 + flags = 1 + data = length 196, hash CF2002D7 + sample 103: + time = 1494666 + flags = 1 + data = length 185, hash E03B5D7 + sample 104: + time = 1516000 + flags = 1 + data = length 187, hash DA399A2C + sample 105: + time = 1537333 + flags = 1 + data = length 191, hash 292AA0DB + sample 106: + time = 1558666 + flags = 1 + data = length 201, hash 221910E0 + sample 107: + time = 1580000 + flags = 1 + data = length 194, hash F4ED7821 + sample 108: + time = 1601333 + flags = 1 + data = length 43, hash FDDA515E + sample 109: + time = 1613333 + flags = 1 + data = length 42, hash F3571C0A + sample 110: + time = 1616000 + flags = 1 + data = length 38, hash 39F910B3 + sample 111: + time = 1618666 + flags = 1 + data = length 41, hash 2D189531 + sample 112: + time = 1621333 + flags = 1 + data = length 43, hash 1F7574DB + sample 113: + time = 1624000 + flags = 1 + data = length 43, hash 644D15E5 + sample 114: + time = 1626666 + flags = 1 + data = length 49, hash E8A0878 + sample 115: + time = 1629333 + flags = 1 + data = length 55, hash DFF2046D + sample 116: + time = 1632000 + flags = 1 + data = length 49, hash 9FB8A23 + sample 117: + time = 1634666 + flags = 1 + data = length 41, hash E3E15E3B + sample 118: + time = 1637333 + flags = 1 + data = length 42, hash E5D17A32 + sample 119: + time = 1640000 + flags = 1 + data = length 42, hash F308B653 + sample 120: + time = 1642666 + flags = 1 + data = length 55, hash BB750D76 + sample 121: + time = 1645333 + flags = 1 + data = length 51, hash 96772ABF + sample 122: + time = 1648000 + flags = 1 + data = length 197, hash E4524346 + sample 123: + time = 1660000 + flags = 1 + data = length 188, hash AC3E1BB5 + sample 124: + time = 1681333 + flags = 1 + data = length 195, hash F56DB8A5 + sample 125: + time = 1702666 + flags = 1 + data = length 198, hash C8970FF7 + sample 126: + time = 1724000 + flags = 1 + data = length 202, hash AF425C68 + sample 127: + time = 1745333 + flags = 1 + data = length 196, hash 4215D839 + sample 128: + time = 1766666 + flags = 1 + data = length 204, hash DB9BE8E3 + sample 129: + time = 1788000 + flags = 1 + data = length 206, hash E5B20AB8 + sample 130: + time = 1809333 + flags = 1 + data = length 209, hash D7F47B95 + sample 131: + time = 1830666 + flags = 1 + data = length 193, hash FB54FB05 + sample 132: + time = 1852000 + flags = 1 + data = length 199, hash D99C3106 + sample 133: + time = 1873333 + flags = 1 + data = length 206, hash 253885B9 + sample 134: + time = 1894666 + flags = 1 + data = length 191, hash FBDD8162 + sample 135: + time = 1916000 + flags = 1 + data = length 183, hash 7290332F + sample 136: + time = 1937333 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 137: + time = 1958666 + flags = 1 + data = length 201, hash 5D936764 + sample 138: + time = 1980000 + flags = 1 + data = length 193, hash 6B03E75E + sample 139: + time = 2001333 + flags = 1 + data = length 199, hash 8A21BA83 + sample 140: + time = 2022666 + flags = 1 + data = length 41, hash E6362210 + sample 141: + time = 2034666 + flags = 1 + data = length 43, hash 36A57B44 + sample 142: + time = 2037333 + flags = 1 + data = length 43, hash E51797D5 + sample 143: + time = 2040000 + flags = 1 + data = length 43, hash 1F336C72 + sample 144: + time = 2042666 + flags = 1 + data = length 42, hash 201AD367 + sample 145: + time = 2045333 + flags = 1 + data = length 50, hash 606CCD6 + sample 146: + time = 2048000 + flags = 1 + data = length 56, hash B15EBD7A + sample 147: + time = 2050666 + flags = 1 + data = length 212, hash 273B8D22 + sample 148: + time = 2062666 + flags = 1 + data = length 194, hash 44F9CE1 + sample 149: + time = 2084000 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 150: + time = 2105333 + flags = 1 + data = length 194, hash CE9F2D26 + sample 151: + time = 2126666 + flags = 1 + data = length 192, hash 204F8A23 + sample 152: + time = 2148000 + flags = 1 + data = length 206, hash DFA57E67 + sample 153: + time = 2169333 + flags = 1 + data = length 196, hash 3CF084AB + sample 154: + time = 2190666 + flags = 1 + data = length 202, hash 2AF75C08 + sample 155: + time = 2212000 + flags = 1 + data = length 203, hash 748EAF7 + sample 156: + time = 2233333 + flags = 1 + data = length 205, hash ED82379D + sample 157: + time = 2254666 + flags = 1 + data = length 193, hash 61F26F22 + sample 158: + time = 2276000 + flags = 1 + data = length 189, hash 85EF1D20 + sample 159: + time = 2297333 + flags = 1 + data = length 187, hash 25E41FBF + sample 160: + time = 2318666 + flags = 1 + data = length 199, hash F365808 + sample 161: + time = 2340000 + flags = 1 + data = length 197, hash 94205329 + sample 162: + time = 2361333 + flags = 1 + data = length 201, hash FA2B2055 + sample 163: + time = 2382666 + flags = 1 + data = length 194, hash AF95381F + sample 164: + time = 2404000 + flags = 1 + data = length 201, hash 923D3534 + sample 165: + time = 2425333 + flags = 1 + data = length 198, hash 35F84C2E + sample 166: + time = 2446666 + flags = 1 + data = length 204, hash 6642CA40 + sample 167: + time = 2468000 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 168: + time = 2489333 + flags = 1 + data = length 197, hash B1E458CE + sample 169: + time = 2510666 + flags = 1 + data = length 193, hash E9218C84 + sample 170: + time = 2532000 + flags = 1 + data = length 192, hash FEF08D4B + sample 171: + time = 2553333 + flags = 1 + data = length 201, hash FC411147 + sample 172: + time = 2574666 + flags = 1 + data = length 218, hash 86893464 + sample 173: + time = 2596000 + flags = 1 + data = length 226, hash 31C5320 + sample 174: + time = 2617333 + flags = 1 + data = length 233, hash 9432BEE5 + sample 175: + time = 2638666 + flags = 1 + data = length 213, hash B3FCC53E + sample 176: + time = 2660000 + flags = 1 + data = length 204, hash D70DD5A2 + sample 177: + time = 2681333 + flags = 1 + data = length 212, hash A4EF1B69 + sample 178: + time = 2702666 + flags = 1 + data = length 203, hash 8B0748B5 + sample 179: + time = 2724000 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump new file mode 100644 index 00000000000..1a71ebbb10c --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump @@ -0,0 +1,456 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 17598 + sample count = 109 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 905333 + flags = 1 + data = length 195, hash 2722159A + sample 1: + time = 926666 + flags = 1 + data = length 199, hash 10CEE97A + sample 2: + time = 948000 + flags = 1 + data = length 191, hash 2CF9FB3F + sample 3: + time = 969333 + flags = 1 + data = length 197, hash A725DA0 + sample 4: + time = 990666 + flags = 1 + data = length 211, hash D4E5DB9E + sample 5: + time = 1012000 + flags = 1 + data = length 189, hash 1A90F496 + sample 6: + time = 1033333 + flags = 1 + data = length 187, hash 44DB2689 + sample 7: + time = 1054666 + flags = 1 + data = length 197, hash 6D3E5117 + sample 8: + time = 1076000 + flags = 1 + data = length 208, hash 5B57B288 + sample 9: + time = 1097333 + flags = 1 + data = length 198, hash D5FC05 + sample 10: + time = 1118666 + flags = 1 + data = length 192, hash 350BBA45 + sample 11: + time = 1140000 + flags = 1 + data = length 195, hash 5F96F2A8 + sample 12: + time = 1161333 + flags = 1 + data = length 202, hash 61D7CC33 + sample 13: + time = 1182666 + flags = 1 + data = length 202, hash 49D335F2 + sample 14: + time = 1204000 + flags = 1 + data = length 192, hash 2FE9CB1A + sample 15: + time = 1225333 + flags = 1 + data = length 201, hash BF0763B2 + sample 16: + time = 1246666 + flags = 1 + data = length 184, hash AD047421 + sample 17: + time = 1268000 + flags = 1 + data = length 196, hash F9088F14 + sample 18: + time = 1289333 + flags = 1 + data = length 190, hash AC6D38FD + sample 19: + time = 1310666 + flags = 1 + data = length 195, hash 8D1A66D2 + sample 20: + time = 1332000 + flags = 1 + data = length 197, hash B46BFB6B + sample 21: + time = 1353333 + flags = 1 + data = length 195, hash D9761F23 + sample 22: + time = 1374666 + flags = 1 + data = length 204, hash 3391B617 + sample 23: + time = 1396000 + flags = 1 + data = length 42, hash 33A1FB52 + sample 24: + time = 1408000 + flags = 1 + data = length 44, hash 408B146E + sample 25: + time = 1410666 + flags = 1 + data = length 44, hash 171C7E0D + sample 26: + time = 1413333 + flags = 1 + data = length 54, hash 6307E16C + sample 27: + time = 1416000 + flags = 1 + data = length 53, hash 4A319572 + sample 28: + time = 1418666 + flags = 1 + data = length 215, hash BA9C445C + sample 29: + time = 1430666 + flags = 1 + data = length 201, hash 3120D234 + sample 30: + time = 1452000 + flags = 1 + data = length 187, hash DB44993C + sample 31: + time = 1473333 + flags = 1 + data = length 196, hash CF2002D7 + sample 32: + time = 1494666 + flags = 1 + data = length 185, hash E03B5D7 + sample 33: + time = 1516000 + flags = 1 + data = length 187, hash DA399A2C + sample 34: + time = 1537333 + flags = 1 + data = length 191, hash 292AA0DB + sample 35: + time = 1558666 + flags = 1 + data = length 201, hash 221910E0 + sample 36: + time = 1580000 + flags = 1 + data = length 194, hash F4ED7821 + sample 37: + time = 1601333 + flags = 1 + data = length 43, hash FDDA515E + sample 38: + time = 1613333 + flags = 1 + data = length 42, hash F3571C0A + sample 39: + time = 1616000 + flags = 1 + data = length 38, hash 39F910B3 + sample 40: + time = 1618666 + flags = 1 + data = length 41, hash 2D189531 + sample 41: + time = 1621333 + flags = 1 + data = length 43, hash 1F7574DB + sample 42: + time = 1624000 + flags = 1 + data = length 43, hash 644D15E5 + sample 43: + time = 1626666 + flags = 1 + data = length 49, hash E8A0878 + sample 44: + time = 1629333 + flags = 1 + data = length 55, hash DFF2046D + sample 45: + time = 1632000 + flags = 1 + data = length 49, hash 9FB8A23 + sample 46: + time = 1634666 + flags = 1 + data = length 41, hash E3E15E3B + sample 47: + time = 1637333 + flags = 1 + data = length 42, hash E5D17A32 + sample 48: + time = 1640000 + flags = 1 + data = length 42, hash F308B653 + sample 49: + time = 1642666 + flags = 1 + data = length 55, hash BB750D76 + sample 50: + time = 1645333 + flags = 1 + data = length 51, hash 96772ABF + sample 51: + time = 1648000 + flags = 1 + data = length 197, hash E4524346 + sample 52: + time = 1660000 + flags = 1 + data = length 188, hash AC3E1BB5 + sample 53: + time = 1681333 + flags = 1 + data = length 195, hash F56DB8A5 + sample 54: + time = 1702666 + flags = 1 + data = length 198, hash C8970FF7 + sample 55: + time = 1724000 + flags = 1 + data = length 202, hash AF425C68 + sample 56: + time = 1745333 + flags = 1 + data = length 196, hash 4215D839 + sample 57: + time = 1766666 + flags = 1 + data = length 204, hash DB9BE8E3 + sample 58: + time = 1788000 + flags = 1 + data = length 206, hash E5B20AB8 + sample 59: + time = 1809333 + flags = 1 + data = length 209, hash D7F47B95 + sample 60: + time = 1830666 + flags = 1 + data = length 193, hash FB54FB05 + sample 61: + time = 1852000 + flags = 1 + data = length 199, hash D99C3106 + sample 62: + time = 1873333 + flags = 1 + data = length 206, hash 253885B9 + sample 63: + time = 1894666 + flags = 1 + data = length 191, hash FBDD8162 + sample 64: + time = 1916000 + flags = 1 + data = length 183, hash 7290332F + sample 65: + time = 1937333 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 66: + time = 1958666 + flags = 1 + data = length 201, hash 5D936764 + sample 67: + time = 1980000 + flags = 1 + data = length 193, hash 6B03E75E + sample 68: + time = 2001333 + flags = 1 + data = length 199, hash 8A21BA83 + sample 69: + time = 2022666 + flags = 1 + data = length 41, hash E6362210 + sample 70: + time = 2034666 + flags = 1 + data = length 43, hash 36A57B44 + sample 71: + time = 2037333 + flags = 1 + data = length 43, hash E51797D5 + sample 72: + time = 2040000 + flags = 1 + data = length 43, hash 1F336C72 + sample 73: + time = 2042666 + flags = 1 + data = length 42, hash 201AD367 + sample 74: + time = 2045333 + flags = 1 + data = length 50, hash 606CCD6 + sample 75: + time = 2048000 + flags = 1 + data = length 56, hash B15EBD7A + sample 76: + time = 2050666 + flags = 1 + data = length 212, hash 273B8D22 + sample 77: + time = 2062666 + flags = 1 + data = length 194, hash 44F9CE1 + sample 78: + time = 2084000 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 79: + time = 2105333 + flags = 1 + data = length 194, hash CE9F2D26 + sample 80: + time = 2126666 + flags = 1 + data = length 192, hash 204F8A23 + sample 81: + time = 2148000 + flags = 1 + data = length 206, hash DFA57E67 + sample 82: + time = 2169333 + flags = 1 + data = length 196, hash 3CF084AB + sample 83: + time = 2190666 + flags = 1 + data = length 202, hash 2AF75C08 + sample 84: + time = 2212000 + flags = 1 + data = length 203, hash 748EAF7 + sample 85: + time = 2233333 + flags = 1 + data = length 205, hash ED82379D + sample 86: + time = 2254666 + flags = 1 + data = length 193, hash 61F26F22 + sample 87: + time = 2276000 + flags = 1 + data = length 189, hash 85EF1D20 + sample 88: + time = 2297333 + flags = 1 + data = length 187, hash 25E41FBF + sample 89: + time = 2318666 + flags = 1 + data = length 199, hash F365808 + sample 90: + time = 2340000 + flags = 1 + data = length 197, hash 94205329 + sample 91: + time = 2361333 + flags = 1 + data = length 201, hash FA2B2055 + sample 92: + time = 2382666 + flags = 1 + data = length 194, hash AF95381F + sample 93: + time = 2404000 + flags = 1 + data = length 201, hash 923D3534 + sample 94: + time = 2425333 + flags = 1 + data = length 198, hash 35F84C2E + sample 95: + time = 2446666 + flags = 1 + data = length 204, hash 6642CA40 + sample 96: + time = 2468000 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 97: + time = 2489333 + flags = 1 + data = length 197, hash B1E458CE + sample 98: + time = 2510666 + flags = 1 + data = length 193, hash E9218C84 + sample 99: + time = 2532000 + flags = 1 + data = length 192, hash FEF08D4B + sample 100: + time = 2553333 + flags = 1 + data = length 201, hash FC411147 + sample 101: + time = 2574666 + flags = 1 + data = length 218, hash 86893464 + sample 102: + time = 2596000 + flags = 1 + data = length 226, hash 31C5320 + sample 103: + time = 2617333 + flags = 1 + data = length 233, hash 9432BEE5 + sample 104: + time = 2638666 + flags = 1 + data = length 213, hash B3FCC53E + sample 105: + time = 2660000 + flags = 1 + data = length 204, hash D70DD5A2 + sample 106: + time = 2681333 + flags = 1 + data = length 212, hash A4EF1B69 + sample 107: + time = 2702666 + flags = 1 + data = length 203, hash 8B0748B5 + sample 108: + time = 2724000 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump new file mode 100644 index 00000000000..50b21ade226 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump @@ -0,0 +1,216 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 8658 + sample count = 49 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 1821333 + flags = 1 + data = length 193, hash FB54FB05 + sample 1: + time = 1842666 + flags = 1 + data = length 199, hash D99C3106 + sample 2: + time = 1864000 + flags = 1 + data = length 206, hash 253885B9 + sample 3: + time = 1885333 + flags = 1 + data = length 191, hash FBDD8162 + sample 4: + time = 1906666 + flags = 1 + data = length 183, hash 7290332F + sample 5: + time = 1928000 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 6: + time = 1949333 + flags = 1 + data = length 201, hash 5D936764 + sample 7: + time = 1970666 + flags = 1 + data = length 193, hash 6B03E75E + sample 8: + time = 1992000 + flags = 1 + data = length 199, hash 8A21BA83 + sample 9: + time = 2013333 + flags = 1 + data = length 41, hash E6362210 + sample 10: + time = 2025333 + flags = 1 + data = length 43, hash 36A57B44 + sample 11: + time = 2028000 + flags = 1 + data = length 43, hash E51797D5 + sample 12: + time = 2030666 + flags = 1 + data = length 43, hash 1F336C72 + sample 13: + time = 2033333 + flags = 1 + data = length 42, hash 201AD367 + sample 14: + time = 2036000 + flags = 1 + data = length 50, hash 606CCD6 + sample 15: + time = 2038666 + flags = 1 + data = length 56, hash B15EBD7A + sample 16: + time = 2041333 + flags = 1 + data = length 212, hash 273B8D22 + sample 17: + time = 2053333 + flags = 1 + data = length 194, hash 44F9CE1 + sample 18: + time = 2074666 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 19: + time = 2096000 + flags = 1 + data = length 194, hash CE9F2D26 + sample 20: + time = 2117333 + flags = 1 + data = length 192, hash 204F8A23 + sample 21: + time = 2138666 + flags = 1 + data = length 206, hash DFA57E67 + sample 22: + time = 2160000 + flags = 1 + data = length 196, hash 3CF084AB + sample 23: + time = 2181333 + flags = 1 + data = length 202, hash 2AF75C08 + sample 24: + time = 2202666 + flags = 1 + data = length 203, hash 748EAF7 + sample 25: + time = 2224000 + flags = 1 + data = length 205, hash ED82379D + sample 26: + time = 2245333 + flags = 1 + data = length 193, hash 61F26F22 + sample 27: + time = 2266666 + flags = 1 + data = length 189, hash 85EF1D20 + sample 28: + time = 2288000 + flags = 1 + data = length 187, hash 25E41FBF + sample 29: + time = 2309333 + flags = 1 + data = length 199, hash F365808 + sample 30: + time = 2330666 + flags = 1 + data = length 197, hash 94205329 + sample 31: + time = 2352000 + flags = 1 + data = length 201, hash FA2B2055 + sample 32: + time = 2373333 + flags = 1 + data = length 194, hash AF95381F + sample 33: + time = 2394666 + flags = 1 + data = length 201, hash 923D3534 + sample 34: + time = 2416000 + flags = 1 + data = length 198, hash 35F84C2E + sample 35: + time = 2437333 + flags = 1 + data = length 204, hash 6642CA40 + sample 36: + time = 2458666 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 37: + time = 2480000 + flags = 1 + data = length 197, hash B1E458CE + sample 38: + time = 2501333 + flags = 1 + data = length 193, hash E9218C84 + sample 39: + time = 2522666 + flags = 1 + data = length 192, hash FEF08D4B + sample 40: + time = 2544000 + flags = 1 + data = length 201, hash FC411147 + sample 41: + time = 2565333 + flags = 1 + data = length 218, hash 86893464 + sample 42: + time = 2586666 + flags = 1 + data = length 226, hash 31C5320 + sample 43: + time = 2608000 + flags = 1 + data = length 233, hash 9432BEE5 + sample 44: + time = 2629333 + flags = 1 + data = length 213, hash B3FCC53E + sample 45: + time = 2650666 + flags = 1 + data = length 204, hash D70DD5A2 + sample 46: + time = 2672000 + flags = 1 + data = length 212, hash A4EF1B69 + sample 47: + time = 2693333 + flags = 1 + data = length 203, hash 8B0748B5 + sample 48: + time = 2714666 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump new file mode 100644 index 00000000000..1d76d892d38 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump @@ -0,0 +1,20 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump new file mode 100644 index 00000000000..9830a08357d --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump @@ -0,0 +1,737 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 26873 + sample count = 180 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 0 + flags = 1 + data = length 49, hash 2FFF94F0 + sample 1: + time = 0 + flags = 1 + data = length 44, hash 3946418A + sample 2: + time = 2666 + flags = 1 + data = length 55, hash 2A0B878E + sample 3: + time = 5333 + flags = 1 + data = length 53, hash CC3B6879 + sample 4: + time = 8000 + flags = 1 + data = length 215, hash 106AE950 + sample 5: + time = 20000 + flags = 1 + data = length 192, hash 2B219F53 + sample 6: + time = 41333 + flags = 1 + data = length 197, hash FBC39422 + sample 7: + time = 62666 + flags = 1 + data = length 209, hash 386E8979 + sample 8: + time = 84000 + flags = 1 + data = length 42, hash E81162C1 + sample 9: + time = 96000 + flags = 1 + data = length 41, hash F15BEE36 + sample 10: + time = 98666 + flags = 1 + data = length 42, hash D67EB19 + sample 11: + time = 101333 + flags = 1 + data = length 42, hash F4DE4792 + sample 12: + time = 104000 + flags = 1 + data = length 53, hash 80F66AC3 + sample 13: + time = 106666 + flags = 1 + data = length 56, hash DCB9DFC4 + sample 14: + time = 109333 + flags = 1 + data = length 55, hash 4E0C4E9D + sample 15: + time = 112000 + flags = 1 + data = length 203, hash 176B6862 + sample 16: + time = 124000 + flags = 1 + data = length 193, hash AB13CB10 + sample 17: + time = 145333 + flags = 1 + data = length 203, hash DE63DE9F + sample 18: + time = 166666 + flags = 1 + data = length 194, hash 4A9508A2 + sample 19: + time = 188000 + flags = 1 + data = length 210, hash 196899B3 + sample 20: + time = 209333 + flags = 1 + data = length 195, hash B68407F1 + sample 21: + time = 230666 + flags = 1 + data = length 193, hash A1FA86E3 + sample 22: + time = 252000 + flags = 1 + data = length 194, hash 5C0B9343 + sample 23: + time = 273333 + flags = 1 + data = length 198, hash 789914B2 + sample 24: + time = 294666 + flags = 1 + data = length 183, hash 1B82D11F + sample 25: + time = 316000 + flags = 1 + data = length 199, hash D5B848F4 + sample 26: + time = 337333 + flags = 1 + data = length 192, hash B34427EA + sample 27: + time = 358666 + flags = 1 + data = length 199, hash C2599BB5 + sample 28: + time = 380000 + flags = 1 + data = length 195, hash BFD83194 + sample 29: + time = 401333 + flags = 1 + data = length 199, hash C9A7F7CA + sample 30: + time = 422666 + flags = 1 + data = length 44, hash 5D76EAD6 + sample 31: + time = 434666 + flags = 1 + data = length 43, hash 8619C423 + sample 32: + time = 437333 + flags = 1 + data = length 43, hash E490BBE + sample 33: + time = 440000 + flags = 1 + data = length 53, hash 8A557CAE + sample 34: + time = 442666 + flags = 1 + data = length 56, hash 81007BBA + sample 35: + time = 445333 + flags = 1 + data = length 56, hash 4E4DD67F + sample 36: + time = 448000 + flags = 1 + data = length 222, hash 414188AB + sample 37: + time = 460000 + flags = 1 + data = length 202, hash 67A07D30 + sample 38: + time = 481333 + flags = 1 + data = length 200, hash E357D853 + sample 39: + time = 502666 + flags = 1 + data = length 203, hash 4653DC90 + sample 40: + time = 524000 + flags = 1 + data = length 192, hash A65E6C09 + sample 41: + time = 545333 + flags = 1 + data = length 202, hash FBEAC508 + sample 42: + time = 566666 + flags = 1 + data = length 202, hash E9B7B59F + sample 43: + time = 588000 + flags = 1 + data = length 204, hash E24AA78E + sample 44: + time = 609333 + flags = 1 + data = length 41, hash 3FBC5216 + sample 45: + time = 621333 + flags = 1 + data = length 47, hash 153FBC55 + sample 46: + time = 624000 + flags = 1 + data = length 42, hash 2B493D6C + sample 47: + time = 626666 + flags = 1 + data = length 42, hash 8303BEE3 + sample 48: + time = 629333 + flags = 1 + data = length 62, hash 71AEE50B + sample 49: + time = 632000 + flags = 1 + data = length 54, hash 52F61908 + sample 50: + time = 634666 + flags = 1 + data = length 45, hash 7BD3E3A1 + sample 51: + time = 637333 + flags = 1 + data = length 41, hash E0F65472 + sample 52: + time = 640000 + flags = 1 + data = length 45, hash 41838675 + sample 53: + time = 642666 + flags = 1 + data = length 44, hash FCBC2147 + sample 54: + time = 645333 + flags = 1 + data = length 45, hash 1A5987E3 + sample 55: + time = 648000 + flags = 1 + data = length 43, hash 99074864 + sample 56: + time = 650666 + flags = 1 + data = length 57, hash D4A9B60A + sample 57: + time = 653333 + flags = 1 + data = length 52, hash 302129DA + sample 58: + time = 656000 + flags = 1 + data = length 57, hash D8DD99C0 + sample 59: + time = 658666 + flags = 1 + data = length 206, hash F4B9EF26 + sample 60: + time = 670666 + flags = 1 + data = length 197, hash 7B8ACC8A + sample 61: + time = 692000 + flags = 1 + data = length 186, hash 161027CB + sample 62: + time = 713333 + flags = 1 + data = length 186, hash 1D6871B6 + sample 63: + time = 734666 + flags = 1 + data = length 201, hash 536E9FDB + sample 64: + time = 756000 + flags = 1 + data = length 192, hash D38EFAC5 + sample 65: + time = 777333 + flags = 1 + data = length 194, hash 4B394EF3 + sample 66: + time = 798666 + flags = 1 + data = length 206, hash 1B31BA99 + sample 67: + time = 820000 + flags = 1 + data = length 212, hash AD061F43 + sample 68: + time = 841333 + flags = 1 + data = length 180, hash 6D1F7481 + sample 69: + time = 862666 + flags = 1 + data = length 195, hash D80B21F + sample 70: + time = 884000 + flags = 1 + data = length 186, hash D367882 + sample 71: + time = 905333 + flags = 1 + data = length 195, hash 2722159A + sample 72: + time = 926666 + flags = 1 + data = length 199, hash 10CEE97A + sample 73: + time = 948000 + flags = 1 + data = length 191, hash 2CF9FB3F + sample 74: + time = 969333 + flags = 1 + data = length 197, hash A725DA0 + sample 75: + time = 990666 + flags = 1 + data = length 211, hash D4E5DB9E + sample 76: + time = 1012000 + flags = 1 + data = length 189, hash 1A90F496 + sample 77: + time = 1033333 + flags = 1 + data = length 187, hash 44DB2689 + sample 78: + time = 1054666 + flags = 1 + data = length 197, hash 6D3E5117 + sample 79: + time = 1076000 + flags = 1 + data = length 208, hash 5B57B288 + sample 80: + time = 1097333 + flags = 1 + data = length 198, hash D5FC05 + sample 81: + time = 1118666 + flags = 1 + data = length 192, hash 350BBA45 + sample 82: + time = 1140000 + flags = 1 + data = length 195, hash 5F96F2A8 + sample 83: + time = 1161333 + flags = 1 + data = length 202, hash 61D7CC33 + sample 84: + time = 1182666 + flags = 1 + data = length 202, hash 49D335F2 + sample 85: + time = 1204000 + flags = 1 + data = length 192, hash 2FE9CB1A + sample 86: + time = 1225333 + flags = 1 + data = length 201, hash BF0763B2 + sample 87: + time = 1246666 + flags = 1 + data = length 184, hash AD047421 + sample 88: + time = 1268000 + flags = 1 + data = length 196, hash F9088F14 + sample 89: + time = 1289333 + flags = 1 + data = length 190, hash AC6D38FD + sample 90: + time = 1310666 + flags = 1 + data = length 195, hash 8D1A66D2 + sample 91: + time = 1332000 + flags = 1 + data = length 197, hash B46BFB6B + sample 92: + time = 1353333 + flags = 1 + data = length 195, hash D9761F23 + sample 93: + time = 1374666 + flags = 1 + data = length 204, hash 3391B617 + sample 94: + time = 1396000 + flags = 1 + data = length 42, hash 33A1FB52 + sample 95: + time = 1408000 + flags = 1 + data = length 44, hash 408B146E + sample 96: + time = 1410666 + flags = 1 + data = length 44, hash 171C7E0D + sample 97: + time = 1413333 + flags = 1 + data = length 54, hash 6307E16C + sample 98: + time = 1416000 + flags = 1 + data = length 53, hash 4A319572 + sample 99: + time = 1418666 + flags = 1 + data = length 215, hash BA9C445C + sample 100: + time = 1430666 + flags = 1 + data = length 201, hash 3120D234 + sample 101: + time = 1452000 + flags = 1 + data = length 187, hash DB44993C + sample 102: + time = 1473333 + flags = 1 + data = length 196, hash CF2002D7 + sample 103: + time = 1494666 + flags = 1 + data = length 185, hash E03B5D7 + sample 104: + time = 1516000 + flags = 1 + data = length 187, hash DA399A2C + sample 105: + time = 1537333 + flags = 1 + data = length 191, hash 292AA0DB + sample 106: + time = 1558666 + flags = 1 + data = length 201, hash 221910E0 + sample 107: + time = 1580000 + flags = 1 + data = length 194, hash F4ED7821 + sample 108: + time = 1601333 + flags = 1 + data = length 43, hash FDDA515E + sample 109: + time = 1613333 + flags = 1 + data = length 42, hash F3571C0A + sample 110: + time = 1616000 + flags = 1 + data = length 38, hash 39F910B3 + sample 111: + time = 1618666 + flags = 1 + data = length 41, hash 2D189531 + sample 112: + time = 1621333 + flags = 1 + data = length 43, hash 1F7574DB + sample 113: + time = 1624000 + flags = 1 + data = length 43, hash 644D15E5 + sample 114: + time = 1626666 + flags = 1 + data = length 49, hash E8A0878 + sample 115: + time = 1629333 + flags = 1 + data = length 55, hash DFF2046D + sample 116: + time = 1632000 + flags = 1 + data = length 49, hash 9FB8A23 + sample 117: + time = 1634666 + flags = 1 + data = length 41, hash E3E15E3B + sample 118: + time = 1637333 + flags = 1 + data = length 42, hash E5D17A32 + sample 119: + time = 1640000 + flags = 1 + data = length 42, hash F308B653 + sample 120: + time = 1642666 + flags = 1 + data = length 55, hash BB750D76 + sample 121: + time = 1645333 + flags = 1 + data = length 51, hash 96772ABF + sample 122: + time = 1648000 + flags = 1 + data = length 197, hash E4524346 + sample 123: + time = 1660000 + flags = 1 + data = length 188, hash AC3E1BB5 + sample 124: + time = 1681333 + flags = 1 + data = length 195, hash F56DB8A5 + sample 125: + time = 1702666 + flags = 1 + data = length 198, hash C8970FF7 + sample 126: + time = 1724000 + flags = 1 + data = length 202, hash AF425C68 + sample 127: + time = 1745333 + flags = 1 + data = length 196, hash 4215D839 + sample 128: + time = 1766666 + flags = 1 + data = length 204, hash DB9BE8E3 + sample 129: + time = 1788000 + flags = 1 + data = length 206, hash E5B20AB8 + sample 130: + time = 1809333 + flags = 1 + data = length 209, hash D7F47B95 + sample 131: + time = 1830666 + flags = 1 + data = length 193, hash FB54FB05 + sample 132: + time = 1852000 + flags = 1 + data = length 199, hash D99C3106 + sample 133: + time = 1873333 + flags = 1 + data = length 206, hash 253885B9 + sample 134: + time = 1894666 + flags = 1 + data = length 191, hash FBDD8162 + sample 135: + time = 1916000 + flags = 1 + data = length 183, hash 7290332F + sample 136: + time = 1937333 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 137: + time = 1958666 + flags = 1 + data = length 201, hash 5D936764 + sample 138: + time = 1980000 + flags = 1 + data = length 193, hash 6B03E75E + sample 139: + time = 2001333 + flags = 1 + data = length 199, hash 8A21BA83 + sample 140: + time = 2022666 + flags = 1 + data = length 41, hash E6362210 + sample 141: + time = 2034666 + flags = 1 + data = length 43, hash 36A57B44 + sample 142: + time = 2037333 + flags = 1 + data = length 43, hash E51797D5 + sample 143: + time = 2040000 + flags = 1 + data = length 43, hash 1F336C72 + sample 144: + time = 2042666 + flags = 1 + data = length 42, hash 201AD367 + sample 145: + time = 2045333 + flags = 1 + data = length 50, hash 606CCD6 + sample 146: + time = 2048000 + flags = 1 + data = length 56, hash B15EBD7A + sample 147: + time = 2050666 + flags = 1 + data = length 212, hash 273B8D22 + sample 148: + time = 2062666 + flags = 1 + data = length 194, hash 44F9CE1 + sample 149: + time = 2084000 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 150: + time = 2105333 + flags = 1 + data = length 194, hash CE9F2D26 + sample 151: + time = 2126666 + flags = 1 + data = length 192, hash 204F8A23 + sample 152: + time = 2148000 + flags = 1 + data = length 206, hash DFA57E67 + sample 153: + time = 2169333 + flags = 1 + data = length 196, hash 3CF084AB + sample 154: + time = 2190666 + flags = 1 + data = length 202, hash 2AF75C08 + sample 155: + time = 2212000 + flags = 1 + data = length 203, hash 748EAF7 + sample 156: + time = 2233333 + flags = 1 + data = length 205, hash ED82379D + sample 157: + time = 2254666 + flags = 1 + data = length 193, hash 61F26F22 + sample 158: + time = 2276000 + flags = 1 + data = length 189, hash 85EF1D20 + sample 159: + time = 2297333 + flags = 1 + data = length 187, hash 25E41FBF + sample 160: + time = 2318666 + flags = 1 + data = length 199, hash F365808 + sample 161: + time = 2340000 + flags = 1 + data = length 197, hash 94205329 + sample 162: + time = 2361333 + flags = 1 + data = length 201, hash FA2B2055 + sample 163: + time = 2382666 + flags = 1 + data = length 194, hash AF95381F + sample 164: + time = 2404000 + flags = 1 + data = length 201, hash 923D3534 + sample 165: + time = 2425333 + flags = 1 + data = length 198, hash 35F84C2E + sample 166: + time = 2446666 + flags = 1 + data = length 204, hash 6642CA40 + sample 167: + time = 2468000 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 168: + time = 2489333 + flags = 1 + data = length 197, hash B1E458CE + sample 169: + time = 2510666 + flags = 1 + data = length 193, hash E9218C84 + sample 170: + time = 2532000 + flags = 1 + data = length 192, hash FEF08D4B + sample 171: + time = 2553333 + flags = 1 + data = length 201, hash FC411147 + sample 172: + time = 2574666 + flags = 1 + data = length 218, hash 86893464 + sample 173: + time = 2596000 + flags = 1 + data = length 226, hash 31C5320 + sample 174: + time = 2617333 + flags = 1 + data = length 233, hash 9432BEE5 + sample 175: + time = 2638666 + flags = 1 + data = length 213, hash B3FCC53E + sample 176: + time = 2660000 + flags = 1 + data = length 204, hash D70DD5A2 + sample 177: + time = 2681333 + flags = 1 + data = length 212, hash A4EF1B69 + sample 178: + time = 2702666 + flags = 1 + data = length 203, hash 8B0748B5 + sample 179: + time = 2724000 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/media/ogg/bear_vorbis_with_large_metadata.ogg b/testdata/src/test/assets/media/ogg/bear_vorbis_with_large_metadata.ogg new file mode 100644 index 0000000000000000000000000000000000000000..1dab9e5258f29ca10e11aaf115fd06b5616e4ab0 GIT binary patch literal 111383 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 OCUj~chimUR=>GxJczsv^ literal 0 HcmV?d00001 From 9ce2ac8a8cb2b45426b3fe269e9845b8b9d0daa6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 30 Sep 2020 16:29:38 +0100 Subject: [PATCH 33/69] Create a robolectricutils module This holds shared test infrastructure that needs to depend on Robolectric. PiperOrigin-RevId: 334604041 --- core_settings.gradle | 2 ++ library/core/build.gradle | 1 + .../exoplayer2/e2etest/Mp4PlaybackTest.java | 4 +-- .../exoplayer2/e2etest/TsPlaybackTest.java | 4 +-- robolectricutils/README.md | 10 ++++++ robolectricutils/build.gradle | 35 +++++++++++++++++++ robolectricutils/src/main/AndroidManifest.xml | 17 +++++++++ .../robolectric}/PlaybackOutput.java | 2 +- .../robolectric}/ShadowMediaCodecConfig.java | 2 +- .../exoplayer2/robolectric}/TeeCodec.java | 2 +- .../exoplayer2/robolectric/package-info.java | 19 ++++++++++ 11 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 robolectricutils/README.md create mode 100644 robolectricutils/build.gradle create mode 100644 robolectricutils/src/main/AndroidManifest.xml rename {library/core/src/test/java/com/google/android/exoplayer2/e2etest/util => robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric}/PlaybackOutput.java (98%) rename {library/core/src/test/java/com/google/android/exoplayer2/e2etest/util => robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric}/ShadowMediaCodecConfig.java (98%) rename {library/core/src/test/java/com/google/android/exoplayer2/e2etest/util => robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric}/TeeCodec.java (98%) create mode 100644 robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java diff --git a/core_settings.gradle b/core_settings.gradle index b5082433712..bd217a37e56 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -29,6 +29,7 @@ include modulePrefix + 'library-extractor' include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' +include modulePrefix + 'robolectricutils' include modulePrefix + 'testutils' include modulePrefix + 'testdata' include modulePrefix + 'extension-av1' @@ -56,6 +57,7 @@ project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'libr project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') +project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata') project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') diff --git a/library/core/build.gradle b/library/core/build.gradle index ddeb734947c..45c8e785c62 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -71,6 +71,7 @@ dependencies { testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils') + testImplementation project(modulePrefix + 'robolectricutils') } ext { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java index f37610d982c..5fd7453beb8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java @@ -23,8 +23,8 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.e2etest.util.PlaybackOutput; -import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.robolectric.PlaybackOutput; +import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.TestExoPlayer; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java index d57f06ff52a..52184f57510 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java @@ -22,8 +22,8 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.e2etest.util.PlaybackOutput; -import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.robolectric.PlaybackOutput; +import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.TestExoPlayer; diff --git a/robolectricutils/README.md b/robolectricutils/README.md new file mode 100644 index 00000000000..430a907c2d6 --- /dev/null +++ b/robolectricutils/README.md @@ -0,0 +1,10 @@ +# ExoPlayer Robolectric utils + +Provides test infrastructure for ExoPlayer Robolectric-based tests. + +## Links + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.robolectric` + belong to this module. + +[Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/robolectricutils/build.gradle b/robolectricutils/build.gradle new file mode 100644 index 00000000000..f5a86822b72 --- /dev/null +++ b/robolectricutils/build.gradle @@ -0,0 +1,35 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" + +dependencies { + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'org.robolectric:robolectric:' + robolectricVersion + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'testutils') +} + +ext { + javadocTitle = 'Robolectric utils' +} +apply from: '../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-robolectricutils' + releaseDescription = 'Robolectric utils for ExoPlayer.' +} +apply from: '../publish.gradle' diff --git a/robolectricutils/src/main/AndroidManifest.xml b/robolectricutils/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..0548a1b32b1 --- /dev/null +++ b/robolectricutils/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java rename to robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java index f9c32d34b56..264b4bcc2f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.e2etest.util; +package com.google.android.exoplayer2.robolectric; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java rename to robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java index 6d7f23107e7..697c4e83161 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.e2etest.util; +package com.google.android.exoplayer2.robolectric; import android.media.MediaCodecInfo; import android.media.MediaFormat; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java rename to robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java index a14787e959a..172350414ef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.e2etest.util; +package com.google.android.exoplayer2.robolectric; import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.util.MimeTypes; diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java new file mode 100644 index 00000000000..0dd7ab81ae6 --- /dev/null +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +@NonNullApi +package com.google.android.exoplayer2.robolectric; + +import com.google.android.exoplayer2.util.NonNullApi; From b8c8ce0ee0c9c6cd32b947ec5da3d4fe6c983864 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 1 Oct 2020 10:22:06 +0100 Subject: [PATCH 34/69] Use Mp4WebvttDecoder for WebVTT content in DASH MP4 containers This was broken by https://github.com/google/ExoPlayer/commit/74a9d8f680995f2096c59fde6cd1ef6e85bb4d55 because DashManifestParser switched to setting Format.sampleMimeType to text/vtt while SubtitleDecoderFactory was still expecting application/x-mp4-vtt. This change teaches SubtitleDecoderFactory to check both Format.containerMimeType and Format.sampleMimeType. I'll investigate a follow-up change to remove MimeTypes.APPLICATION_MP4VTT completely (it's currently still used in AtomParsers). Issue: #7985 PiperOrigin-RevId: 334771672 --- RELEASENOTES.md | 2 + .../text/SubtitleDecoderFactory.java | 16 ++-- library/dash/build.gradle | 1 + .../exoplayer2/e2etest/DashPlaybackTest.java | 73 ++++++++++++++++++ .../robolectric/PlaybackOutput.java | 63 ++++++++++++++- .../media/dash/webvtt-in-mp4/sample.mpd | 23 ++++++ .../media/dash/webvtt-in-mp4/sample.text.mp4 | Bin 0 -> 1006 bytes .../media/dash/webvtt-in-mp4/sample.video.mp4 | Bin 0 -> 91242 bytes .../playbackdumps/dash/webvtt-in-mp4.dump | 58 ++++++++++++++ 9 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java create mode 100644 testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd create mode 100644 testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.text.mp4 create mode 100644 testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.video.mp4 create mode 100644 testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3404a3e7d96..53e4e257e7f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). + * Fix WebVTT subtitles in MP4 containers in DASH streams + ([#7985](https://github.com/google/ExoPlayer/issues/7985)). * UI: * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index bd652c65863..e59a7489bb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -91,11 +91,15 @@ public boolean supportsFormat(Format format) { @Override public SubtitleDecoder createDecoder(Format format) { - @Nullable String mimeType = format.sampleMimeType; - if (mimeType != null) { - switch (mimeType) { + @Nullable String sampleMimeType = format.sampleMimeType; + if (sampleMimeType != null) { + switch (sampleMimeType) { case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); + if (MimeTypes.APPLICATION_MP4.equals(format.containerMimeType)) { + return new Mp4WebvttDecoder(); + } else { + return new WebvttDecoder(); + } case MimeTypes.TEXT_SSA: return new SsaDecoder(format.initializationData); case MimeTypes.APPLICATION_MP4VTT: @@ -109,7 +113,7 @@ public SubtitleDecoder createDecoder(Format format) { case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: return new Cea608Decoder( - mimeType, + sampleMimeType, format.accessibilityChannel, Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); case MimeTypes.APPLICATION_CEA708: @@ -123,7 +127,7 @@ public SubtitleDecoder createDecoder(Format format) { } } throw new IllegalArgumentException( - "Attempted to create decoder for unsupported MIME type: " + mimeType); + "Attempted to create decoder for unsupported MIME type: " + sampleMimeType); } }; } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index e6cb20d9334..e34ab3f9db2 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -36,6 +36,7 @@ dependencies { compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + testImplementation project(modulePrefix + 'robolectricutils') testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java new file mode 100644 index 00000000000..e0ea43b1147 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java @@ -0,0 +1,73 @@ +/* + * 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.e2etest; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + +import android.graphics.SurfaceTexture; +import android.view.Surface; +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.testutil.AutoAdvancingFakeClock; +import com.google.android.exoplayer2.testutil.DumpFileAsserts; +import com.google.android.exoplayer2.testutil.TestExoPlayer; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** End-to-end tests using DASH samples. */ +// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved. +@Config(sdk = 29) +@RunWith(AndroidJUnit4.class) +public final class DashPlaybackTest { + + @Rule + public ShadowMediaCodecConfig mediaCodecConfig = + ShadowMediaCodecConfig.forAllSupportedMimeTypes(); + + // https://github.com/google/ExoPlayer/issues/7985 + @Test + public void webvttInMp4() throws Exception { + SimpleExoPlayer player = + new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) + .setClock(new AutoAdvancingFakeClock()) + .build(); + player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig); + + // Ensure the subtitle track is selected. + DefaultTrackSelector trackSelector = + checkNotNull((DefaultTrackSelector) player.getTrackSelector()); + trackSelector.setParameters(trackSelector.buildUponParameters().setPreferredTextLanguage("en")); + player.setMediaItem(MediaItem.fromUri("asset:///media/dash/webvtt-in-mp4/sample.mpd")); + player.prepare(); + player.play(); + TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + ApplicationProvider.getApplicationContext(), + playbackOutput, + "playbackdumps/dash/webvtt-in-mp4.dump"); + } +} diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java index 264b4bcc2f3..64ff61cb222 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.robolectric; +import android.graphics.Bitmap; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.testutil.Dumper; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,17 +40,19 @@ public final class PlaybackOutput implements Dumper.Dumpable { private final ShadowMediaCodecConfig codecConfig; - // TODO: Add support for subtitles too private final List metadatas; + private final List> subtitles; private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) { this.codecConfig = codecConfig; metadatas = Collections.synchronizedList(new ArrayList<>()); + subtitles = Collections.synchronizedList(new ArrayList<>()); // TODO: Consider passing playback position into MetadataOutput and TextOutput. Calling // player.getCurrentPosition() inside onMetadata/Cues will likely be non-deterministic // because renderer-thread != playback-thread. player.addMetadataOutput(metadatas::add); + player.addTextOutput(subtitles::add); } /** @@ -74,6 +81,7 @@ public void dump(Dumper dumper) { } dumpMetadata(dumper); + dumpSubtitles(dumper); } private void dumpMetadata(Dumper dumper) { @@ -91,4 +99,57 @@ private void dumpMetadata(Dumper dumper) { } dumper.endBlock(); } + + private void dumpSubtitles(Dumper dumper) { + if (subtitles.isEmpty()) { + return; + } + dumper.startBlock("TextOutput"); + for (int i = 0; i < subtitles.size(); i++) { + dumper.startBlock("Subtitle[" + i + "]"); + List subtitle = subtitles.get(i); + if (subtitle.isEmpty()) { + dumper.add("Cues", ImmutableList.of()); + } + for (int j = 0; j < subtitle.size(); j++) { + dumper.startBlock("Cue[" + j + "]"); + Cue cue = subtitle.get(j); + dumpIfNotEqual(dumper, "text", cue.text, null); + dumpIfNotEqual(dumper, "textAlignment", cue.textAlignment, null); + dumpBitmap(dumper, cue.bitmap); + dumpIfNotEqual(dumper, "line", cue.line, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "lineType", cue.lineType, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "lineAnchor", cue.lineAnchor, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "position", cue.position, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "positionAnchor", cue.positionAnchor, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "size", cue.size, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "bitmapHeight", cue.bitmapHeight, Cue.DIMEN_UNSET); + if (cue.windowColorSet) { + dumper.add("cue.windowColor", cue.windowColor); + } + dumpIfNotEqual(dumper, "textSizeType", cue.textSizeType, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "textSize", cue.textSize, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "verticalType", cue.verticalType, Cue.TYPE_UNSET); + dumper.endBlock(); + } + dumper.endBlock(); + } + dumper.endBlock(); + } + + private static void dumpIfNotEqual( + Dumper dumper, String field, @Nullable Object actual, @Nullable Object comparison) { + if (!Util.areEqual(actual, comparison)) { + dumper.add(field, actual); + } + } + + private static void dumpBitmap(Dumper dumper, @Nullable Bitmap bitmap) { + if (bitmap == null) { + return; + } + byte[] bytes = new byte[bitmap.getByteCount()]; + bitmap.copyPixelsToBuffer(ByteBuffer.wrap(bytes)); + dumper.add("bitmap", bytes); + } } diff --git a/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd new file mode 100644 index 00000000000..fae0dc98eca --- /dev/null +++ b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd @@ -0,0 +1,23 @@ + + + + + + + + sample.text.mp4 + + + + + + + + sample.video.mp4 + + + + + + + diff --git a/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.text.mp4 b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.text.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..44844168f87ef92b18d9857b103ac5f29818712d GIT binary patch literal 1006 zcmZuv&2G~`5S|7kI8-QyRz-+|1-&&tN*bwEFH|i^g8#`dNrVdT|(-?uZfyW;_XAi|3*aj5|^@`olH4Ww{!Xrd6H z_1UCpL0j4svE-N^AHVEl=AxU4a4u%bLNV-( zdV_9wus;|M4!TxpC7hzCy+JGHRO|)L%Ld%IY5+fd-AZwFXN4dCleTfk$V8A782SGu zKl@T9behdTb&kz46XK7>LTY*q*;kV+BJ#Vk@^__G%-Q%ho4M6Njo&+*YQiTDUHOx1 zefX+Ce+K8=B6|A#(aG6lLTszhV(QY|hUz6D#TD8P(UJE?+|d{j2TZzJn1xJTHZ#hGo4r0)*%5!!q~XK@S<;#*pLr#)KyzMHl00BnEq+(^MS x_NcSmr~8?>(33cE0eu*&AWE#mz~xg+P^%uNQ78>2bn2Ad)fA_~bRpMs{sO!7u+9Jg literal 0 HcmV?d00001 diff --git a/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.video.mp4 b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..7f456f85f90287364aa84b1ff6d39e31137ee8aa GIT binary patch literal 91242 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 literal 0 HcmV?d00001 diff --git a/testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump b/testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump new file mode 100644 index 00000000000..3fbd68f8dbe --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump @@ -0,0 +1,58 @@ +MediaCodec (video/avc): + buffers.length = 31 + buffers[0] = length 36692, hash D216076E + buffers[1] = length 5312, hash D45D3CA0 + buffers[2] = length 599, hash 1BE7812D + buffers[3] = length 7735, hash 4490F110 + buffers[4] = length 987, hash 560B5036 + buffers[5] = length 673, hash ED7CD8C7 + buffers[6] = length 523, hash 3020DF50 + buffers[7] = length 6061, hash 736C72B2 + buffers[8] = length 992, hash FE132F23 + buffers[9] = length 623, hash 5B2C1816 + buffers[10] = length 421, hash 742E69C1 + buffers[11] = length 4899, hash F72F86A1 + buffers[12] = length 568, hash 519A8E50 + buffers[13] = length 620, hash 3990AA39 + buffers[14] = length 5450, hash F06EC4AA + buffers[15] = length 1051, hash 92DFA63A + buffers[16] = length 874, hash 69587FB4 + buffers[17] = length 781, hash 36BE495B + buffers[18] = length 4725, hash AC0C8CD3 + buffers[19] = length 1022, hash 5D8BFF34 + buffers[20] = length 790, hash 99413A99 + buffers[21] = length 610, hash 5E129290 + buffers[22] = length 2751, hash 769974CB + buffers[23] = length 745, hash B78A477A + buffers[24] = length 621, hash CF741E7A + buffers[25] = length 505, hash 1DB4894E + buffers[26] = length 1268, hash C15348DC + buffers[27] = length 880, hash C2DE85D0 + buffers[28] = length 530, hash C98BC6A8 + buffers[29] = length 568, hash 4FE5C8EA + buffers[30] = length 0, hash 1 +TextOutput: + Subtitle[0]: + Cues = [] + Subtitle[1]: + Cue[0]: + text = This is the first subtitle. + textAlignment = ALIGN_CENTER + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[2]: + Cues = [] + Subtitle[3]: + Cue[0]: + text = This is the second subtitle. + textAlignment = ALIGN_CENTER + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[4]: + Cues = [] From 13d886022107965bdac7bd3680378a2bca45fc60 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 1 Oct 2020 11:08:36 +0100 Subject: [PATCH 35/69] Tweak null-checking in TextRenderer#getNextEventTime() `subtitle` is only guaranteed to be non-null if `nextSubtitleEventIndex != C.INDEX_UNSET`. The null check added in https://github.com/google/ExoPlayer/commit/0efec5f6c12a5d583f24c122fbcbc1b1eebbabc3 was too early. Issue: #8017 PiperOrigin-RevId: 334777742 --- RELEASENOTES.md | 2 ++ .../com/google/android/exoplayer2/text/TextRenderer.java | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 53e4e257e7f..cb279f5cd16 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ * Add support for `\h` SSA/ASS style override code (non-breaking space). * Fix WebVTT subtitles in MP4 containers in DASH streams ([#7985](https://github.com/google/ExoPlayer/issues/7985)). + * Fix NPE in `TextRenderer` when playing content with a single subtitle + buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). * UI: * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 6c140c74d17..76c13600457 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -325,10 +325,13 @@ private void replaceDecoder() { } private long getNextEventTime() { + if (nextSubtitleEventIndex == C.INDEX_UNSET) { + return Long.MAX_VALUE; + } checkNotNull(subtitle); - return nextSubtitleEventIndex == C.INDEX_UNSET - || nextSubtitleEventIndex >= subtitle.getEventTimeCount() - ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex); + return nextSubtitleEventIndex >= subtitle.getEventTimeCount() + ? Long.MAX_VALUE + : subtitle.getEventTime(nextSubtitleEventIndex); } private void updateOutput(List cues) { From 88abe26ec38ddeb20cb353e8e75d97d65a2c4629 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Oct 2020 14:03:37 +0100 Subject: [PATCH 36/69] Allow apps to add a VideoAdPlayerCallback Issue: #7944 PiperOrigin-RevId: 335012643 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cb279f5cd16..3b2aa2e1589 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,8 @@ * IMA extension: * Fix position reporting after fetch errors ([#7956](https://github.com/google/ExoPlayer/issues/7956)). + * Allow apps to specify a `VideoAdPlayerCallback` + ([#7944](https://github.com/google/ExoPlayer/issues/7944)). ### 2.12.0 (2020-09-11) ### 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 592920bfc48..ffece0f110e 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 @@ -125,6 +125,7 @@ public static final class Builder { @Nullable private ImaSdkSettings imaSdkSettings; @Nullable private AdErrorListener adErrorListener; @Nullable private AdEventListener adEventListener; + @Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback; @Nullable private Set adUiElements; @Nullable private Collection companionAdSlots; private long adPreloadTimeoutMs; @@ -190,6 +191,22 @@ public Builder setAdEventListener(AdEventListener adEventListener) { return this; } + /** + * Sets a callback to receive video ad player events. Note that these events are handled + * internally by the IMA SDK and this ads loader. For analytics and diagnostics, new + * implementations should generally use events from the top-level {@link Player} listeners + * instead of setting a callback via this method. + * + * @param videoAdPlayerCallback The callback to receive video ad player events. + * @return This builder, for convenience. + * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback + */ + public Builder setVideoAdPlayerCallback( + VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) { + this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback); + return this; + } + /** * Sets the ad UI elements to be rendered by the IMA SDK. * @@ -524,6 +541,9 @@ private ImaAdsLoader(Builder builder, @Nullable Uri adTagUri, @Nullable String a handler = Util.createHandler(getImaLooper(), /* callback= */ null); componentListener = new ComponentListener(); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); + if (builder.videoAdPlayerCallback != null) { + adCallbacks.add(builder.videoAdPlayerCallback); + } updateAdProgressRunnable = this::updateAdProgress; adInfoByAdMediaInfo = HashBiMap.create(); supportedMimeTypes = Collections.emptyList(); From 1cca9ffd01c4b217eea77d5e7239a5921b72f92f Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 6 Oct 2020 11:53:02 +0100 Subject: [PATCH 37/69] Add search bytes parameter to TsExtractor Context: Issue: #7988 PiperOrigin-RevId: 335608610 --- RELEASENOTES.md | 8 +-- .../extractor/DefaultExtractorsFactory.java | 20 +++++++- .../extractor/ts/TsBinarySearchSeeker.java | 18 ++++--- .../extractor/ts/TsDurationReader.java | 10 ++-- .../exoplayer2/extractor/ts/TsExtractor.java | 51 ++++++++++++++++--- .../extractor/ts/TsDurationReaderTest.java | 2 +- 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b2aa2e1589..bffb735a47a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,9 +15,6 @@ ([#7985](https://github.com/google/ExoPlayer/issues/7985)). * Fix NPE in `TextRenderer` when playing content with a single subtitle buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). -* UI: - * Do not require subtitleButton in custom layouts of StyledPlayerView - ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Audio: * Fix the default audio sink position not advancing correctly when using `AudioTrack`-based speed adjustment @@ -29,7 +26,12 @@ ([#7949](https://github.com/google/ExoPlayer/issues/7949)). * Fix regression for Ogg files with packets that span multiple pages ([#7992](https://github.com/google/ExoPlayer/issues/7992)). + * Add TS extractor parameter to configure the number of bytes in which + to search for a timestamp to determine the duration and to seek. + ([#7988](https://github.com/google/ExoPlayer/issues/7988)). * UI + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 2eba1b1cca0..2068853d9ea 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -131,9 +131,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Mp3Extractor.Flags private int mp3Flags; @TsExtractor.Mode private int tsMode; @DefaultTsPayloadReaderFactory.Flags private int tsFlags; + private int tsTimestampSearchBytes; public DefaultExtractorsFactory() { tsMode = TsExtractor.MODE_SINGLE_PMT; + tsTimestampSearchBytes = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES; } /** @@ -246,7 +248,7 @@ public synchronized DefaultExtractorsFactory setMp3ExtractorFlags(@Mp3Extractor. /** * Sets the mode for {@link TsExtractor} instances created by the factory. * - * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory) + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory, int) * @param mode The mode to use. * @return The factory, for convenience. */ @@ -269,6 +271,20 @@ public synchronized DefaultExtractorsFactory setTsExtractorFlags( return this; } + /** + * Sets the number of bytes searched to find a timestamp for {@link TsExtractor} instances created + * by the factory. + * + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory, int) + * @param timestampSearchBytes The number of search bytes to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorTimestampSearchBytes( + int timestampSearchBytes) { + tsTimestampSearchBytes = timestampSearchBytes; + return this; + } + @Override public synchronized Extractor[] createExtractors() { return createExtractors(Uri.EMPTY, new HashMap<>()); @@ -361,7 +377,7 @@ private void addExtractorsForFileType(@FileTypes.Type int fileType, ListGiven a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link - * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to + * #timestampSearchBytes} from that stream position, look for all packets with PID equal to * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target * timestamp. */ @@ -67,10 +70,13 @@ private static final class TsPcrSeeker implements TimestampSeeker { private final TimestampAdjuster pcrTimestampAdjuster; private final ParsableByteArray packetBuffer; private final int pcrPid; + private final int timestampSearchBytes; - public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { + public TsPcrSeeker( + int pcrPid, TimestampAdjuster pcrTimestampAdjuster, int timestampSearchBytes) { this.pcrPid = pcrPid; this.pcrTimestampAdjuster = pcrTimestampAdjuster; + this.timestampSearchBytes = timestampSearchBytes; packetBuffer = new ParsableByteArray(); } @@ -78,7 +84,7 @@ public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) throws IOException { long inputPosition = input.getPosition(); - int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); + int bytesToSearch = (int) min(timestampSearchBytes, input.getLength() - inputPosition); packetBuffer.reset(bytesToSearch); input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java index 5020f4c76da..504b84d575c 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java @@ -38,8 +38,7 @@ */ /* package */ final class TsDurationReader { - private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; - + private final int timestampSearchBytes; private final TimestampAdjuster pcrTimestampAdjuster; private final ParsableByteArray packetBuffer; @@ -51,7 +50,8 @@ private long lastPcrValue; private long durationUs; - /* package */ TsDurationReader() { + /* package */ TsDurationReader(int timestampSearchBytes) { + this.timestampSearchBytes = timestampSearchBytes; pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); firstPcrValue = C.TIME_UNSET; lastPcrValue = C.TIME_UNSET; @@ -125,7 +125,7 @@ private int finishReadDuration(ExtractorInput input) { private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException { - int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength()); + int bytesToSearch = (int) min(timestampSearchBytes, input.getLength()); int searchStartPosition = 0; if (input.getPosition() != searchStartPosition) { seekPositionHolder.position = searchStartPosition; @@ -161,7 +161,7 @@ private long readFirstPcrValueFromBuffer(ParsableByteArray packetBuffer, int pcr private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException { long inputLength = input.getLength(); - int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, inputLength); + int bytesToSearch = (int) min(timestampSearchBytes, inputLength); long searchStartPosition = inputLength - bytesToSearch; if (input.getPosition() != searchStartPosition) { seekPositionHolder.position = searchStartPosition; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2fcfd422a01..2a9613f7f40 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -80,6 +80,9 @@ public final class TsExtractor implements Extractor { */ public static final int MODE_HLS = 2; + public static final int TS_PACKET_SIZE = 188; + public static final int DEFAULT_TIMESTAMP_SEARCH_BYTES = 600 * TS_PACKET_SIZE; + public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F; @@ -100,7 +103,6 @@ public final class TsExtractor implements Extractor { // Stream types that aren't defined by the MPEG-2 TS specification. public static final int TS_STREAM_TYPE_AIT = 0x101; - public static final int TS_PACKET_SIZE = 188; public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. private static final int TS_PAT_PID = 0; @@ -115,6 +117,7 @@ public final class TsExtractor implements Extractor { private static final int SNIFF_TS_PACKET_COUNT = 5; private final @Mode int mode; + private final int timestampSearchBytes; private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final SparseIntArray continuityCounters; @@ -136,7 +139,7 @@ public final class TsExtractor implements Extractor { private int pcrPid; public TsExtractor() { - this(0); + this(/* defaultTsPayloadReaderFlags= */ 0); } /** @@ -144,7 +147,7 @@ public TsExtractor() { * {@code FLAG_*} values that control the behavior of the payload readers. */ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { - this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags); + this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags, DEFAULT_TIMESTAMP_SEARCH_BYTES); } /** @@ -152,12 +155,22 @@ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { * and {@link #MODE_HLS}. * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} * {@code FLAG_*} values that control the behavior of the payload readers. + * @param timestampSearchBytes The number of bytes searched from a given position in the stream to + * find a PCR timestamp. If this value is too small, the duration might be unknown and seeking + * might not be supported for high bitrate progressive streams. Setting a large value for this + * field might be inefficient though because the extractor stores a buffer of {@code + * timestampSearchBytes} bytes when determining the duration or when performing a seek + * operation. The default value is {@link #DEFAULT_TIMESTAMP_SEARCH_BYTES}. If the number of + * bytes left in the stream from the current position is less than {@code + * timestampSearchBytes}, the search is performed on the bytes left. */ - public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { + public TsExtractor( + @Mode int mode, @Flags int defaultTsPayloadReaderFlags, int timestampSearchBytes) { this( mode, new TimestampAdjuster(0), - new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags), + timestampSearchBytes); } /** @@ -170,7 +183,30 @@ public TsExtractor( @Mode int mode, TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory) { + this(mode, timestampAdjuster, payloadReaderFactory, DEFAULT_TIMESTAMP_SEARCH_BYTES); + } + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param payloadReaderFactory Factory for injecting a custom set of payload readers. + * @param timestampSearchBytes The number of bytes searched from a given position in the stream to + * find a PCR timestamp. If this value is too small, the duration might be unknown and seeking + * might not be supported for high bitrate progressive streams. Setting a large value for this + * field might be inefficient though because the extractor stores a buffer of {@code + * timestampSearchBytes} bytes when determining the duration or when performing a seek + * operation. The default value is {@link #DEFAULT_TIMESTAMP_SEARCH_BYTES}. If the number of + * bytes left in the stream from the current position is less than {@code + * timestampSearchBytes}, the search is performed on the bytes left. + */ + public TsExtractor( + @Mode int mode, + TimestampAdjuster timestampAdjuster, + TsPayloadReader.Factory payloadReaderFactory, + int timestampSearchBytes) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); + this.timestampSearchBytes = timestampSearchBytes; this.mode = mode; if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { timestampAdjusters = Collections.singletonList(timestampAdjuster); @@ -183,7 +219,7 @@ public TsExtractor( trackPids = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); - durationReader = new TsDurationReader(); + durationReader = new TsDurationReader(timestampSearchBytes); pcrPid = -1; resetPayloadReaders(); } @@ -365,7 +401,8 @@ private void maybeOutputSeekMap(long inputLength) { durationReader.getPcrTimestampAdjuster(), durationReader.getDurationUs(), inputLength, - pcrPid); + pcrPid, + timestampSearchBytes); output.seekMap(tsBinarySearchSeeker.getSeekMap()); } else { output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java index 8f744e855d7..0e55d292b8c 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java @@ -37,7 +37,7 @@ public final class TsDurationReaderTest { @Before public void setUp() { - tsDurationReader = new TsDurationReader(); + tsDurationReader = new TsDurationReader(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES); seekPositionHolder = new PositionHolder(); } From bddaaf022ca5584936a3d913eb091d10e5288b9a Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 6 Oct 2020 14:24:37 +0100 Subject: [PATCH 38/69] Ignore negative payload size in TS PesReader Issue: #8005 PiperOrigin-RevId: 335625992 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/ts/PesReader.java | 22 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bffb735a47a..2a00a22b56d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ * Add TS extractor parameter to configure the number of bytes in which to search for a timestamp to determine the duration and to seek. ([#7988](https://github.com/google/ExoPlayer/issues/7988)). + * Ignore negative payload size in PES packets + ([#8005](https://github.com/google/ExoPlayer/issues/8005)). * UI * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 0764087b592..97fe7a73368 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -97,11 +97,11 @@ public final void consume(ParsableByteArray data, @Flags int flags) throws Parse Log.w(TAG, "Unexpected start indicator reading extended header"); break; case STATE_READING_BODY: - // If payloadSize == -1 then the length of the previous packet was unspecified, and so - // we only know that it's finished now that we've seen the start of the next one. This - // is expected. If payloadSize != -1, then the length of the previous packet was known, - // but we didn't receive that amount of data. This is not expected. - if (payloadSize != -1) { + // If payloadSize is unset then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This is + // expected. If payloadSize is set, then the length of the previous packet was known, but + // we didn't receive that amount of data. This is not expected. + if (payloadSize != C.LENGTH_UNSET) { Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); } // Either way, notify the reader that it has now finished. @@ -136,13 +136,13 @@ && continueRead(data, /* target= */ null, extendedHeaderLength)) { break; case STATE_READING_BODY: readLength = data.bytesLeft(); - int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize; if (padding > 0) { readLength -= padding; data.setLimit(data.getPosition() + readLength); } reader.consume(data); - if (payloadSize != -1) { + if (payloadSize != C.LENGTH_UNSET) { payloadSize -= readLength; if (payloadSize == 0) { reader.packetFinished(); @@ -191,7 +191,7 @@ private boolean parseHeader() { int startCodePrefix = pesScratch.readBits(24); if (startCodePrefix != 0x000001) { Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); - payloadSize = -1; + payloadSize = C.LENGTH_UNSET; return false; } @@ -208,10 +208,14 @@ private boolean parseHeader() { extendedHeaderLength = pesScratch.readBits(8); if (packetLength == 0) { - payloadSize = -1; + payloadSize = C.LENGTH_UNSET; } else { payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ - HEADER_SIZE - extendedHeaderLength; + if (payloadSize < 0) { + Log.w(TAG, "Found negative packet payload size: " + payloadSize); + payloadSize = C.LENGTH_UNSET; + } } return true; } From 837cdc4f67eb275ca40d99bc403d779834f0c1e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 6 Oct 2020 15:20:54 +0000 Subject: [PATCH 39/69] Fix miscellaneous nits/typos PiperOrigin-RevId: 335642909 --- .../google/android/exoplayer2/demo/PlayerActivity.java | 2 +- .../exoplayer2/ext/media2/DefaultMediaItemConverter.java | 2 +- .../android/exoplayer2/ext/media2/MediaItemConverter.java | 4 ++-- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 2 +- .../com/google/android/exoplayer2/MediaPeriodQueue.java | 8 ++++---- .../android/exoplayer2/source/ProgressiveMediaSource.java | 2 +- .../exoplayer2/trackselection/MappingTrackSelector.java | 5 +++-- .../android/exoplayer2/trackselection/TrackSelector.java | 2 +- .../exoplayer2/trackselection/TrackSelectorResult.java | 7 ++++--- .../exoplayer2/trackselection/TrackSelectorTest.java | 3 ++- .../exoplayer2/upstream/DefaultBandwidthMeterTest.java | 2 +- .../exoplayer2/video/MediaCodecVideoRendererTest.java | 4 +--- .../com/google/android/exoplayer2/ui/SubtitleView.java | 1 - 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index eae302887e0..8fb92ed2701 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -102,7 +102,7 @@ public class PlayerActivity extends AppCompatActivity private int startWindow; private long startPosition; - // Fields used only for ad playback. The ads loader is loaded via reflection. + // Fields used only for ad playback. private AdsLoader adsLoader; private Uri loadedAdTagUri; diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java index c23bdd56692..e6d4550d884 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java @@ -45,7 +45,7 @@ public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem me if (media2MediaItem instanceof CallbackMediaItem) { throw new IllegalStateException("CallbackMediaItem isn't supported"); } - + @Nullable Uri uri = null; @Nullable String mediaId = null; @Nullable String title = null; diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java index 218c2a737e5..99b284af3c7 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java @@ -23,13 +23,13 @@ */ public interface MediaItemConverter { /** - * Converts an {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem + * Converts a {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem * ExoPlayer MediaItem}. */ MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem); /** - * Converts an {@link MediaItem ExoPlayer MediaItem} to an {@link androidx.media2.common.MediaItem + * Converts an {@link MediaItem ExoPlayer MediaItem} to a {@link androidx.media2.common.MediaItem * Media2 MediaItem}. */ androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem); 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 b1f57364658..21583a4d685 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 @@ -153,7 +153,7 @@ public ExoPlayerImpl( new TrackSelectorResult( new RendererConfiguration[renderers.length], new TrackSelection[renderers.length], - null); + /* info= */ null); period = new Timeline.Period(); maskingWindowIndex = C.INDEX_UNSET; playbackInfoUpdateHandler = new Handler(applicationLooper); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index b64a9c8087a..fa6201bf37f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -315,8 +315,8 @@ public void clear() { public boolean updateQueuedPeriods( Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline - // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be - // handled here. + // is set, once all cases handled by ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed + // can be handled here. MediaPeriodHolder previousPeriodHolder = null; MediaPeriodHolder periodHolder = playing; while (periodHolder != null) { @@ -326,8 +326,8 @@ public boolean updateQueuedPeriods( MediaPeriodInfo newPeriodInfo; if (previousPeriodHolder == null) { // The id and start position of the first period have already been verified by - // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline - // and isLastInPeriod flags. + // ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed. Just update duration, + // isLastInTimeline and isLastInPeriod flags. newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo); } else { newPeriodInfo = 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 4d7230cc3ae..19f09fde22b 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 @@ -340,7 +340,7 @@ private void notifySourceInfoRefreshed() { /* manifest= */ null, mediaItem); if (timelineIsPlaceholder) { - // TODO: Actually prepare the extractors during prepatation so that we don't need a + // TODO: Actually prepare the extractors during preparation so that we don't need a // placeholder. See https://github.com/google/ExoPlayer/issues/4727. timeline = new ForwardingTimeline(timeline) { 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 9949a370ede..16c63353ee4 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 @@ -339,14 +339,15 @@ public TrackGroupArray getUnmappedTrackGroups() { * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ - public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() { + @Nullable + public final MappedTrackInfo getCurrentMappedTrackInfo() { return currentMappedTrackInfo; } // TrackSelector implementation. @Override - public final void onSelectionActivated(Object info) { + public final void onSelectionActivated(@Nullable Object info) { currentMappedTrackInfo = (MappedTrackInfo) info; } 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 8ee9d29d3d2..59c5d5447bf 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 @@ -137,7 +137,7 @@ public abstract TrackSelectorResult selectTracks( * * @param info The value of {@link TrackSelectorResult#info} in the activated selection. */ - public abstract void onSelectionActivated(Object info); + public abstract void onSelectionActivated(@Nullable Object info); /** * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously 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 9228f3af628..67623c2cf6b 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 @@ -40,19 +40,20 @@ public final class TrackSelectorResult { * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)} * should the selections be activated. */ - public final Object info; + @Nullable public final Object info; /** * @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 info An opaque object that will be returned to {@link - * TrackSelector#onSelectionActivated(Object)} should the selection be activated. + * TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be + * {@code null}. */ public TrackSelectorResult( @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] selections, - Object info) { + @Nullable Object info) { this.rendererConfigurations = rendererConfigurations; this.selections = new TrackSelectionArray(selections); this.info = info; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java index 477f7226a46..f4073317112 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.RendererCapabilities; @@ -52,7 +53,7 @@ public TrackSelectorResult selectTracks( } @Override - public void onSelectionActivated(Object info) {} + public void onSelectionActivated(@Nullable Object info) {} }; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java index 23f5a17e93f..0b807c487a4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java @@ -107,7 +107,7 @@ public void setUp() { /* isAvailable= */ true, CONNECTED); } - + @Test public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor2G() { setActiveNetworkInfo(networkInfoWifi); 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 4ba5eb34b1f..74d110516b1 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 @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.testutil.FakeSampleStream; import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; import com.google.android.exoplayer2.util.MimeTypes; @@ -107,8 +106,7 @@ public void setUp() throws Exception { /* maxDroppedFramesToNotify= */ 1) { @Override @Capabilities - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) - throws DecoderQueryException { + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) { return RendererCapabilities.create(FORMAT_HANDLED); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 452be5a3b77..bfd18aead76 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -414,5 +414,4 @@ private Cue removeEmbeddedStyling(Cue cue) { return cue; } - } From 5b0b4479ae7e088d8dfa90ac8a7a043675f8fff5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 7 Oct 2020 10:02:42 +0100 Subject: [PATCH 40/69] Fix HLS chunkful preparation bug affecting certain master playlists The bug affects playlists that start with an I-FRAME only variant. Issue: #8025 PiperOrigin-RevId: 335819497 --- RELEASENOTES.md | 4 ++++ .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a00a22b56d..1f1bb38b996 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,10 @@ * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). +* HLS: + * Fix crash affecting chunkful preparation of master playlists that start + with an I-FRAME only variant + ([#8025](https://github.com/google/ExoPlayer/issues/8025)). * IMA extension: * Fix position reporting after fetch errors ([#7956](https://github.com/google/ExoPlayer/issues/7956)). 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 530d56fa9c8..2ab4852339b 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 @@ -592,7 +592,9 @@ private static final class InitializationTrackSelection extends BaseTrackSelecti public InitializationTrackSelection(TrackGroup group, int[] tracks) { super(group, tracks); - selectedIndex = indexOf(group.getFormat(0)); + // The initially selected index corresponds to the first EXT-X-STREAMINF tag in the master + // playlist. + selectedIndex = indexOf(group.getFormat(tracks[0])); } @Override From c8879392ab9665f0c06d742744f606d2cf359584 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 7 Oct 2020 13:30:05 +0100 Subject: [PATCH 41/69] Make resetPosition reset the position if true Issue: #8024 PiperOrigin-RevId: 335846035 --- .../exoplayer2/ext/cast/CastPlayer.java | 7 ++++ .../google/android/exoplayer2/BasePlayer.java | 6 --- .../android/exoplayer2/ExoPlayerImpl.java | 5 +++ .../android/exoplayer2/ExoPlayerTest.java | 37 +++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 80d9817a463..eeda98d2d9e 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 @@ -307,6 +307,13 @@ public void removeListener(EventListener listener) { } } + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + int windowIndex = resetPosition ? 0 : getCurrentWindowIndex(); + long startPositionMs = resetPosition ? C.TIME_UNSET : getContentPosition(); + setMediaItems(mediaItems, windowIndex, startPositionMs); + } + @Override public void setMediaItems( List mediaItems, int startWindowIndex, long startPositionMs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 9d7af2dce61..4f89925121f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -44,12 +44,6 @@ public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { setMediaItems(Collections.singletonList(mediaItem), resetPosition); } - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - setMediaItems( - mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET); - } - @Override public void setMediaItems(List mediaItems) { setMediaItems(mediaItems, /* resetPosition= */ true); 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 21583a4d685..377863a0837 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 @@ -347,6 +347,11 @@ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean rese prepare(); } + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + setMediaSources(createMediaSources(mediaItems), resetPosition); + } + @Override public void setMediaItems( List mediaItems, int startWindowIndex, long startPositionMs) { 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 7934298df08..ffd46f90891 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 @@ -104,6 +104,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -5699,6 +5700,42 @@ public void run(SimpleExoPlayer player) { assertArrayEquals(new int[] {0, 0, 0}, currentWindowIndices); } + @Test + public void setMediaItems_resetPosition_resetsPosition() throws Exception { + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] currentPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + List listOfTwo = + Lists.newArrayList( + MediaItem.fromUri(Uri.EMPTY), MediaItem.fromUri(Uri.EMPTY)); + player.setMediaItems(listOfTwo, /* resetPosition= */ true); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPositions[1] = player.getCurrentPosition(); + } + }) + .prepare() + .waitForTimelineChanged() + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + assertArrayEquals(new long[] {1000, 0}, currentPositions); + } + @Test public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex() throws Exception { From 1e315e47d642a61a8bd33f993bab1418a91ab1b0 Mon Sep 17 00:00:00 2001 From: insun Date: Thu, 8 Oct 2020 09:58:11 +0100 Subject: [PATCH 42/69] Expand bottom button's height and extend greyed background area to seekbar Adjusted the bottom layout of StyledPlayerControlView : - Enlarged bottom button's height to make tapping easier. - Extended greyed background area to upper edge of seekbar. - Gave padding between bottom edge of the overall layout and bottom buttons. - Reduced horizontal margins between bottom buttons. PiperOrigin-RevId: 336041160 --- RELEASENOTES.md | 16 ++++++++++------ .../layout/exo_styled_player_control_view.xml | 3 ++- library/ui/src/main/res/values/dimens.xml | 15 +++++++++------ library/ui/src/main/res/values/styles.xml | 10 ++++++---- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1f1bb38b996..c31a803c5e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,16 @@ ([#7985](https://github.com/google/ExoPlayer/issues/7985)). * Fix NPE in `TextRenderer` when playing content with a single subtitle buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). +* UI: + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). + * Add the option to sort tracks by `Format` in `TrackSelectionView` and + `TrackSelectionDialogBuilder` + ([#7709](https://github.com/google/ExoPlayer/issues/7709)). + * Adjusted bottom buttons' heights and paddings in StyledPlayerView for + easy tapping. + * Show overflow button in `StyledPlayerControlView` only when there is no + enough space. * Audio: * Fix the default audio sink position not advancing correctly when using `AudioTrack`-based speed adjustment @@ -31,12 +41,6 @@ ([#7988](https://github.com/google/ExoPlayer/issues/7988)). * Ignore negative payload size in PES packets ([#8005](https://github.com/google/ExoPlayer/issues/8005)). -* UI - * Do not require subtitleButton in custom layouts of StyledPlayerView - ([#7962](https://github.com/google/ExoPlayer/issues/7962)). - * Add the option to sort tracks by `Format` in `TrackSelectionView` and - `TrackSelectionDialogBuilder` - ([#7709](https://github.com/google/ExoPlayer/issues/7709)). * HLS: * Fix crash affecting chunkful preparation of master playlists that start with an I-FRAME only variant diff --git a/library/ui/src/main/res/layout/exo_styled_player_control_view.xml b/library/ui/src/main/res/layout/exo_styled_player_control_view.xml index 3136f9d8110..3fb8b98ce55 100644 --- a/library/ui/src/main/res/layout/exo_styled_player_control_view.xml +++ b/library/ui/src/main/res/layout/exo_styled_player_control_view.xml @@ -40,11 +40,12 @@ android:layout_height="@dimen/exo_bottom_bar_height" android:layout_gravity="bottom" android:background="@color/exo_bottom_bar_background" + android:paddingBottom="@dimen/exo_bottom_bar_padding_bottom" android:layoutDirection="ltr"> 8dp 52dp - 5dp + 5dp 2dp 9dp 18dp - 48dp - 32dp + 48dp + 48dp + 2dp 12dp - 4dp + 12dp 2dp 24dp - 40dp + 56dp - 32dp + 70dp + 4dp 10dp 170sp + 48dp 32dp 64dp diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index d86c3e5a39d..03afddfdc53 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -61,8 +61,8 @@ + From d7e8238b88e7ddfa1b7ed37729e4df7c72b41551 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 21 Oct 2020 15:50:35 +0100 Subject: [PATCH 68/69] Bump version to 2.12.1 PiperOrigin-RevId: 338261975 --- RELEASENOTES.md | 74 +++++++++---------- constants.gradle | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +- 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 910781495c6..19ceda94990 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,63 +1,57 @@ # Release notes -### 2.12.1 ### +### 2.12.1 (2020-10-23) ### * Core library: - * Fix bug where streams with highly uneven durations may get stuck in a - buffering state - ([#7943](https://github.com/google/ExoPlayer/issues/7943)). - * Switch Guava dependency from `implementation` to `api` - ([#7905](https://github.com/google/ExoPlayer/issues/7905), - ([#7993](https://github.com/google/ExoPlayer/issues/7993)). + * Fix bug where streams with highly uneven track durations may get stuck + in a buffering state + * Add 403, 500 and 503 to the list of HTTP status codes that can trigger + failover to another quality variant during adaptive playbacks. * Data sources: * Add support for `android.resource` URI scheme in `RawResourceDataSource` ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). - * Fix WebVTT subtitles in MP4 containers in DASH streams + * Fix playback of WebVTT subtitles in MP4 containers in DASH streams ([#7985](https://github.com/google/ExoPlayer/issues/7985)). - * Fix NPE in `TextRenderer` when playing content with a single subtitle - buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). + * Fix `NullPointerException` in `TextRenderer` when playing content with a + single subtitle buffer + ([#8017](https://github.com/google/ExoPlayer/issues/8017)). * UI: - * Show overflow button in `StyledPlayerControlView` only when there is not - enough space. * Fix animation when `StyledPlayerView` first shows its playback controls. - * Allow subtitleButton to be omitted in custom `StyledPlayerView` layouts - ([#7962](https://github.com/google/ExoPlayer/issues/7962)). + * Improve touch targets in `StyledPlayerView` to make tapping easier. + * Allow `subtitleButton` to be omitted in custom `StyledPlayerView` + layouts ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Add an option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). - * Improve touch targets in `StyledPlayerView` to make tapping easier. * Audio: * Fix the default audio sink position not advancing correctly when using - `AudioTrack`-based speed adjustment + `AudioTrack` based speed adjustment ([#7982](https://github.com/google/ExoPlayer/issues/7982)). * Fix `NoClassDefFoundError` warning for `AudioTrack$StreamEventCallback` - even though the class was not used ([#8058](https://github.com/google/ExoPlayer/issues/8058)). * Extractors: - * Add support for `_mp2` boxes in `Mp4Extractor` - ([#7967](https://github.com/google/ExoPlayer/issues/7967)). - * Fix playback of MP4 and MOV files containing `pcm_alaw` or `pcm_mulaw` - audio tracks, by enabling sample rechunking of such tracks - * Use TLEN ID3 tag to compute the duration in `Mp3Extractor` + * MP4: + * Add support for `_mp2` boxes + ([#7967](https://github.com/google/ExoPlayer/issues/7967)). + * Fix playback of files containing `pcm_alaw` or `pcm_mulaw` audio + tracks, by enabling sample rechunking for such tracks. + * MPEG-TS: + * Add `TsExtractor` parameter to configure the number of bytes in + which to search for timestamps when seeking and determining stream + duration ([#7988](https://github.com/google/ExoPlayer/issues/7988)). + * Ignore negative payload size in PES packets + ([#8005](https://github.com/google/ExoPlayer/issues/8005)). + * MP3: Use TLEN ID3 tag to compute the stream duration ([#7949](https://github.com/google/ExoPlayer/issues/7949)). - * Fix regression for Ogg files with packets that span multiple pages + * Ogg: Fix regression playing files with packets that span multiple pages ([#7992](https://github.com/google/ExoPlayer/issues/7992)). - * Add TS extractor parameter to configure the number of bytes in which - to search for a timestamp to determine the duration and to seek. - ([#7988](https://github.com/google/ExoPlayer/issues/7988)). - * Ignore negative payload size in PES packets - ([#8005](https://github.com/google/ExoPlayer/issues/8005)). - * Make FLV files seekable by using the key frame index + * FLV: Make files seekable by using the key frame index ([#7378](https://github.com/google/ExoPlayer/issues/7378)). -* Adaptive playback (DASH / HLS / SmoothStreaming): - * Add 403, 500 and 503 to the list of HTTP status codes that can trigger - failover to another quality variant. -* HLS: - * Fix crash affecting chunkful preparation of master playlists that start - with an I-FRAME only variant - ([#8025](https://github.com/google/ExoPlayer/issues/8025)). +* HLS: Fix crash affecting chunkful preparation of master playlists that start + with an I-FRAME only variant + ([#8025](https://github.com/google/ExoPlayer/issues/8025)). * IMA extension: * Fix position reporting after fetch errors ([#7956](https://github.com/google/ExoPlayer/issues/7956)). @@ -70,7 +64,7 @@ ([#3750](https://github.com/google/ExoPlayer/issues/3750)). * Add a way to override ad media MIME types ([#7961)(https://github.com/google/ExoPlayer/issues/7961)). - * Fix truncating large cue points in microseconds + * Fix incorrect truncation of large cue point positions ([#8067](https://github.com/google/ExoPlayer/issues/8067)). * Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for companion ads rendering when targeting API 29 @@ -235,7 +229,7 @@ To learn more about what's new in 2.12, read the corresponding * Redefine `Cue.lineType=LINE_TYPE_NUMBER` in terms of aligning the cue text lines to grid of viewport lines. Only consider `Cue.lineAnchor` when `Cue.lineType=LINE_TYPE_FRACTION`. - * WebVTT + * WebVTT: * Add support for default [text](https://www.w3.org/TR/webvtt1/#default-text-color) and [background](https://www.w3.org/TR/webvtt1/#default-text-background) @@ -250,10 +244,10 @@ To learn more about what's new in 2.12, read the corresponding * Parse the `ruby-position` CSS property. * Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko). * Parse the `` and `` tags. - * TTML + * TTML: * Parse the `tts:combineText` property (i.e., tate-chu-yoko). * Parse t`tts:ruby` and `tts:rubyPosition` properties. - * CEA-608 + * CEA-608: * Implement timing-out of stuck captions, as permitted by ANSI/CTA-608-E R-2014 Annex C.9. The default timeout is set to 16 seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)). diff --git a/constants.gradle b/constants.gradle index c2b00003680..44a61d6baaa 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.0' - releaseVersionCode = 2012000 + releaseVersion = '2.12.1' + releaseVersionCode = 2012001 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. 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 15c4bf1c1d2..b751fff7bd8 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.0"; + public static final String VERSION = "2.12.1"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.1"; /** * 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 = 2012000; + public static final int VERSION_INT = 2012001; /** The default user agent for requests made by the library. */ public static final String DEFAULT_USER_AGENT = From 269ea4ea2b46d379807cd6f0b86074d0b0fc7eae Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 22 Oct 2020 12:32:37 +0100 Subject: [PATCH 69/69] Add missing release note PiperOrigin-RevId: 338446775 --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 19ceda94990..29c3fcf90f7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,8 @@ ### 2.12.1 (2020-10-23) ### * Core library: + * Fix issue where `Player.setMediaItems` would ignore its `resetPosition` + argument ([#8024](https://github.com/google/ExoPlayer/issues/8024)). * Fix bug where streams with highly uneven track durations may get stuck in a buffering state * Add 403, 500 and 503 to the list of HTTP status codes that can trigger