diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 04bf514c12..a3f6c1ebfc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,36 @@ # Release notes # +### 2.10.4 ### + +* Offline: Add `Scheduler` implementation that uses `WorkManager`. +* Add ability to specify a description when creating notification channels via + ExoPlayer library classes. +* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language + tags instead of 3-letter ISO 639-2 language tags. +* Ensure the `SilenceMediaSource` position is in range + ([#6229](https://github.com/google/ExoPlayer/issues/6229)). +* WAV: Calculate correct duration for clipped streams + ([#6241](https://github.com/google/ExoPlayer/issues/6241)). +* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change + from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). +* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata + ([#5527](https://github.com/google/ExoPlayer/issues/5527)). +* Fix issue where initial seek positions get ignored when playing a preroll ad + ([#6201](https://github.com/google/ExoPlayer/issues/6201)). +* Fix issue where invalid language tags were normalized to "und" instead of + keeping the original + ([#6153](https://github.com/google/ExoPlayer/issues/6153)). +* Fix `DataSchemeDataSource` re-opening and range requests + ([#6192](https://github.com/google/ExoPlayer/issues/6192)). +* Fix Flac and ALAC playback on some LG devices + ([#5938](https://github.com/google/ExoPlayer/issues/5938)). +* Fix issue when calling `performClick` on `PlayerView` without + `PlayerControlView` + ([#6260](https://github.com/google/ExoPlayer/issues/6260)). +* Fix issue where playback speeds are not used in adaptive track selections + after manual selection changes for other renderers + ([#6256](https://github.com/google/ExoPlayer/issues/6256)). + ### 2.10.3 ### * Display last frame when seeking to end of stream diff --git a/build.gradle b/build.gradle index bc538ead68..1d0b459bf5 100644 --- a/build.gradle +++ b/build.gradle @@ -21,14 +21,6 @@ buildscript { classpath 'com.novoda:bintray-release:0.9' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' } - // Workaround for the following test coverage issue. Remove when fixed: - // https://code.google.com/p/android/issues/detail?id=226070 - configurations.all { - resolutionStrategy { - force 'org.jacoco:org.jacoco.report:0.7.4.201502262128' - force 'org.jacoco:org.jacoco.core:0.7.4.201502262128' - } - } } allprojects { repositories { diff --git a/constants.gradle b/constants.gradle index 70e77b22c6..9e532e053b 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.3' - releaseVersionCode = 2010003 + releaseVersion = '2.10.4' + releaseVersionCode = 2010004 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 diff --git a/core_settings.gradle b/core_settings.gradle index 4d90fa962a..38889e1a21 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-jobdispatcher' +include modulePrefix + 'extension-workmanager' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') +project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager') diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 03a54947cf..85e60f2796 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -47,17 +47,6 @@ android { // The demo app isn't indexed and doesn't have translations. disable 'GoogleAppIndexingWarning','MissingTranslation' } - - flavorDimensions "receiver" - - productFlavors { - defaultCast { - dimension "receiver" - manifestPlaceholders = - [castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"] - } - } - } dependencies { diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 856b0b1235..dbfdd833f6 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ android:largeHeap="true" android:allowBackup="false"> + android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> listeners; private final ArrayList notificationsBatch; private final ArrayDeque ongoingNotificationsTasks; - private SessionAvailabilityListener sessionAvailabilityListener; + @Nullable private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. + @Nullable private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; @@ -148,6 +147,7 @@ public final class CastPlayer extends BasePlayer { * starts at position 0. * @return The Cast {@code PendingResult}, or null if no session is available. */ + @Nullable public PendingResult loadItem(MediaQueueItem item, long positionMs) { return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); } @@ -163,8 +163,9 @@ public final class CastPlayer extends BasePlayer { * @param repeatMode The repeat mode for the created media queue. * @return The Cast {@code PendingResult}, or null if no session is available. */ - public PendingResult loadItems(MediaQueueItem[] items, int startIndex, - long positionMs, @RepeatMode int repeatMode) { + @Nullable + public PendingResult loadItems( + MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; waitingForInitialTimeline = true; @@ -180,6 +181,7 @@ public final class CastPlayer extends BasePlayer { * @param items The items to append. * @return The Cast {@code PendingResult}, or null if no media queue exists. */ + @Nullable public PendingResult addItems(MediaQueueItem... items) { return addItems(MediaQueueItem.INVALID_ITEM_ID, items); } @@ -194,6 +196,7 @@ public final class CastPlayer extends BasePlayer { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult addItems(int periodId, MediaQueueItem... items) { if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { @@ -211,6 +214,7 @@ public final class CastPlayer extends BasePlayer { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult removeItem(int periodId) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { return remoteMediaClient.queueRemoveItem(periodId, null); @@ -229,6 +233,7 @@ public final class CastPlayer extends BasePlayer { * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * periodId} exist. */ + @Nullable public PendingResult moveItem(int periodId, int newIndex) { Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { @@ -246,6 +251,7 @@ public final class CastPlayer extends BasePlayer { * @return The item that corresponds to the period with the given id, or null if no media queue or * period with id {@code periodId} exist. */ + @Nullable public MediaQueueItem getItem(int periodId) { MediaStatus mediaStatus = getMediaStatus(); return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET @@ -264,9 +270,9 @@ public final class CastPlayer extends BasePlayer { /** * Sets a listener for updates on the cast session availability. * - * @param listener The {@link SessionAvailabilityListener}. + * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener. */ - public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { + public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) { sessionAvailabilityListener = listener; } @@ -322,6 +328,7 @@ public final class CastPlayer extends BasePlayer { } @Override + @Nullable public ExoPlaybackException getPlaybackError() { return null; } @@ -529,7 +536,7 @@ public final class CastPlayer extends BasePlayer { // Internal methods. - public void updateInternalState() { + private void updateInternalState() { if (remoteMediaClient == null) { // There is no session. We leave the state of the player as it is now. return; @@ -675,7 +682,8 @@ public final class CastPlayer extends BasePlayer { } } - private @Nullable MediaStatus getMediaStatus() { + @Nullable + private MediaStatus getMediaStatus() { return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java index 06f0bec971..4ce45a92b1 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.OptionsProvider; import com.google.android.gms.cast.framework.SessionProvider; +import java.util.Collections; import java.util.List; /** @@ -36,7 +37,7 @@ public final class DefaultCastOptionsProvider implements OptionsProvider { @Override public List getAdditionalSessionProviders(Context context) { - return null; + return Collections.emptyList(); } } diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 76972a3530..b2dd6bc889 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -31,9 +31,9 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:73.3683.76' + api 'org.chromium.net:cronet-embedded:75.3770.101' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index ffecdcd16f..15952b1860 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 7c5864420a..35b67e1068 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -172,28 +172,49 @@ import java.util.List; private static @Nullable byte[] getExtraData(String mimeType, List initializationData) { switch (mimeType) { case MimeTypes.AUDIO_AAC: - case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_OPUS: return initializationData.get(0); + case MimeTypes.AUDIO_ALAC: + return getAlacExtraData(initializationData); case MimeTypes.AUDIO_VORBIS: - byte[] header0 = initializationData.get(0); - byte[] header1 = initializationData.get(1); - byte[] extraData = new byte[header0.length + header1.length + 6]; - extraData[0] = (byte) (header0.length >> 8); - extraData[1] = (byte) (header0.length & 0xFF); - System.arraycopy(header0, 0, extraData, 2, header0.length); - extraData[header0.length + 2] = 0; - extraData[header0.length + 3] = 0; - extraData[header0.length + 4] = (byte) (header1.length >> 8); - extraData[header0.length + 5] = (byte) (header1.length & 0xFF); - System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); - return extraData; + return getVorbisExtraData(initializationData); default: // Other codecs do not require extra data. return null; } } + private static byte[] getAlacExtraData(List initializationData) { + // FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra + // data. initializationData[0] contains only the magic cookie, and so we need to package it into + // an ALAC atom. See: + // https://ffmpeg.org/doxygen/0.6/alac_8c.html + // https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt + byte[] magicCookie = initializationData.get(0); + int alacAtomLength = 12 + magicCookie.length; + ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength); + alacAtom.putInt(alacAtomLength); + alacAtom.putInt(0x616c6163); // type=alac + alacAtom.putInt(0); // version=0, flags=0 + alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length); + return alacAtom.array(); + } + + private static byte[] getVorbisExtraData(List initializationData) { + byte[] header0 = initializationData.get(0); + byte[] header1 = initializationData.get(1); + byte[] extraData = new byte[header0.length + header1.length + 6]; + extraData[0] = (byte) (header0.length >> 8); + extraData[1] = (byte) (header0.length & 0xFF); + System.arraycopy(header0, 0, extraData, 2, header0.length); + extraData[header0.length + 2] = 0; + extraData[header0.length + 3] = 0; + extraData[header0.length + 4] = (byte) (header1.length >> 8); + extraData[header0.length + 5] = (byte) (header1.length & 0xFF); + System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length); + return extraData; + } + private native long ffmpegInitialize( String codecName, @Nullable byte[] extraData, diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 06a5888404..c67de27697 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -39,7 +39,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion testImplementation project(modulePrefix + 'testutils-robolectric') diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index ee0a9fa5b5..3e52f643e7 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -9,6 +9,9 @@ -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } --keep class com.google.android.exoplayer2.util.FlacStreamInfo { +-keep class com.google.android.exoplayer2.util.FlacStreamMetadata { + *; +} +-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame { *; } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java index 435279fc45..a3770afc78 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeekerTest.java @@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest { FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), + /* firstFramePosition= */ 0, + data.length, + decoderJni); SeekMap seekMap = seeker.getSeekMap(); assertThat(seekMap).isNotNull(); @@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest { decoderJni.setData(input); FlacBinarySearchSeeker seeker = new FlacBinarySearchSeeker( - decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); + decoderJni.decodeStreamMetadata(), + /* firstFramePosition= */ 0, + data.length, + decoderJni); seeker.setSeekTargetUs(/* timeUs= */ 1000); assertThat(seeker.isSeeking()).isTrue(); diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index d9cbac6ad5..97f152cea4 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -28,7 +28,7 @@ import org.junit.runner.RunWith; public class FlacExtractorTest { @Before - public void setUp() throws Exception { + public void setUp() { if (!FlacLibrary.isAvailable()) { fail("Flac library not available."); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index b9c6ea06dd..4bfcc003ec 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; @@ -34,20 +34,20 @@ import java.nio.ByteBuffer; private final FlacDecoderJni decoderJni; public FlacBinarySearchSeeker( - FlacStreamInfo streamInfo, + FlacStreamMetadata streamMetadata, long firstFramePosition, long inputLength, FlacDecoderJni decoderJni) { super( - new FlacSeekTimestampConverter(streamInfo), + new FlacSeekTimestampConverter(streamMetadata), new FlacTimestampSeeker(decoderJni), - streamInfo.durationUs(), + streamMetadata.durationUs(), /* floorTimePosition= */ 0, - /* ceilingTimePosition= */ streamInfo.totalSamples, + /* ceilingTimePosition= */ streamMetadata.totalSamples, /* floorBytePosition= */ firstFramePosition, /* ceilingBytePosition= */ inputLength, - /* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), - /* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); + /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize)); this.decoderJni = Assertions.checkNotNull(decoderJni); } @@ -112,15 +112,15 @@ import java.nio.ByteBuffer; * the timestamp for a stream seek time position. */ private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { - private final FlacStreamInfo streamInfo; + private final FlacStreamMetadata streamMetadata; - public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; + public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) { + this.streamMetadata = streamMetadata; } @Override public long timeUsToTargetTime(long timeUs) { - return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); + return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs); } } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 2d74bce5f1..50eb048d98 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -15,11 +15,13 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; @@ -56,21 +58,20 @@ import java.util.List; } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamInfo streamInfo; + FlacStreamMetadata streamMetadata; try { - streamInfo = decoderJni.decodeMetadata(); + streamMetadata = decoderJni.decodeStreamMetadata(); + } catch (ParserException e) { + throw new FlacDecoderException("Failed to decode StreamInfo", e); } catch (IOException | InterruptedException e) { // Never happens. throw new IllegalStateException(e); } - if (streamInfo == null) { - throw new FlacDecoderException("Metadata decoding failed"); - } int initialInputBufferSize = - maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; + maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); + maxOutputBufferSize = streamMetadata.maxDecodedFrameSize(); } @Override @@ -94,6 +95,7 @@ import java.util.List; } @Override + @Nullable protected FlacDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index de038921aa..f454e28c68 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -15,9 +15,12 @@ */ package com.google.android.exoplayer2.ext.flac; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; @@ -37,14 +40,14 @@ import java.nio.ByteBuffer; } } - private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has + private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac. private final long nativeDecoderContext; - private ByteBuffer byteBufferData; - private ExtractorInput extractorInput; + @Nullable private ByteBuffer byteBufferData; + @Nullable private ExtractorInput extractorInput; + @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; - private byte[] tempBuffer; public FlacDecoderJni() throws FlacDecoderException { if (!FlacLibrary.isAvailable()) { @@ -57,67 +60,79 @@ import java.nio.ByteBuffer; } /** - * Sets data to be parsed by libflac. - * @param byteBufferData Source {@link ByteBuffer} + * Sets the data to be parsed. + * + * @param byteBufferData Source {@link ByteBuffer}. */ public void setData(ByteBuffer byteBufferData) { this.byteBufferData = byteBufferData; this.extractorInput = null; - this.tempBuffer = null; } /** - * Sets data to be parsed by libflac. - * @param extractorInput Source {@link ExtractorInput} + * Sets the data to be parsed. + * + * @param extractorInput Source {@link ExtractorInput}. */ public void setData(ExtractorInput extractorInput) { this.byteBufferData = null; this.extractorInput = extractorInput; - if (tempBuffer == null) { - this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; - } endOfExtractorInput = false; + if (tempBuffer == null) { + tempBuffer = new byte[TEMP_BUFFER_SIZE]; + } } + /** + * Returns whether the end of the data to be parsed has been reached, or true if no data was set. + */ public boolean isEndOfData() { if (byteBufferData != null) { return byteBufferData.remaining() == 0; } else if (extractorInput != null) { return endOfExtractorInput; + } else { + return true; } - return true; + } + + /** Clears the data to be parsed. */ + public void clearData() { + byteBufferData = null; + extractorInput = null; } /** * Reads up to {@code length} bytes from the data source. - *

- * This method blocks until at least one byte of data can be read, the end of the input is + * + *

This method blocks until at least one byte of data can be read, the end of the input is * detected or an exception is thrown. - *

- * This method is called from the native code. * * @param target A target {@link ByteBuffer} into which data should be written. - * @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns - * zero; it just means all the data read from the source. + * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been + * read from the source, then 0 is returned. */ + @SuppressWarnings("unused") // Called from native code. public int read(ByteBuffer target) throws IOException, InterruptedException { int byteCount = target.remaining(); if (byteBufferData != null) { byteCount = Math.min(byteCount, byteBufferData.remaining()); int originalLimit = byteBufferData.limit(); byteBufferData.limit(byteBufferData.position() + byteCount); - target.put(byteBufferData); - byteBufferData.limit(originalLimit); } else if (extractorInput != null) { + ExtractorInput extractorInput = this.extractorInput; + byte[] tempBuffer = Util.castNonNull(this.tempBuffer); byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); - int read = readFromExtractorInput(0, byteCount); + int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount); if (read < 4) { // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in // the buffer of the input. Do another read to reduce the number of calls to this method // from the native code. - read += readFromExtractorInput(read, byteCount - read); + read += + readFromExtractorInput( + extractorInput, tempBuffer, read, /* length= */ byteCount - read); } byteCount = read; target.put(tempBuffer, 0, byteCount); @@ -127,9 +142,13 @@ import java.nio.ByteBuffer; return byteCount; } - /** Decodes and consumes the StreamInfo section from the FLAC stream. */ - public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { - return flacDecodeMetadata(nativeDecoderContext); + /** Decodes and consumes the metadata from the FLAC stream. */ + public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException { + FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext); + if (streamMetadata == null) { + throw new ParserException("Failed to decode stream metadata"); + } + return streamMetadata; } /** @@ -234,7 +253,8 @@ import java.nio.ByteBuffer; flacRelease(nativeDecoderContext); } - private int readFromExtractorInput(int offset, int length) + private int readFromExtractorInput( + ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) throws IOException, InterruptedException { int read = extractorInput.read(tempBuffer, offset, length); if (read == C.RESULT_END_OF_INPUT) { @@ -246,7 +266,7 @@ import java.nio.ByteBuffer; private native long flacInit(); - private native FlacStreamInfo flacDecodeMetadata(long context) + private native FlacStreamMetadata flacDecodeMetadata(long context) throws IOException, InterruptedException; private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index bb72e114fe..cd91b06288 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -21,7 +21,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -33,7 +33,8 @@ import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -42,6 +43,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Facilitates the extraction of data from the FLAC container format. @@ -74,23 +78,20 @@ public final class FlacExtractor implements Extractor { */ private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; + private final ParsableByteArray outputBuffer; private final Id3Peeker id3Peeker; - private final boolean isId3MetadataDisabled; + private final boolean id3MetadataDisabled; - private FlacDecoderJni decoderJni; + @Nullable private FlacDecoderJni decoderJni; + private @MonotonicNonNull ExtractorOutput extractorOutput; + private @MonotonicNonNull TrackOutput trackOutput; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; + private boolean streamMetadataDecoded; + private @MonotonicNonNull FlacStreamMetadata streamMetadata; + private @MonotonicNonNull OutputFrameHolder outputFrameHolder; - private ParsableByteArray outputBuffer; - private ByteBuffer outputByteBuffer; - private BinarySearchSeeker.OutputFrameHolder outputFrameHolder; - private FlacStreamInfo streamInfo; - - private Metadata id3Metadata; - private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker; - - private boolean readPastStreamInfo; + @Nullable private Metadata id3Metadata; + @Nullable private FlacBinarySearchSeeker binarySearchSeeker; /** Constructs an instance with flags = 0. */ public FlacExtractor() { @@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor { * @param flags Flags that control the extractor's behavior. */ public FlacExtractor(int flags) { + outputBuffer = new ParsableByteArray(); id3Peeker = new Id3Peeker(); - isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @Override @@ -130,48 +132,53 @@ public final class FlacExtractor implements Extractor { @Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) { + if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { id3Metadata = peekId3Data(input); } - decoderJni.setData(input); - readPastStreamInfo(input); - - if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) { - return handlePendingSeek(input, seekPosition); - } - - long lastDecodePosition = decoderJni.getDecodePosition(); + FlacDecoderJni decoderJni = initDecoderJni(input); try { - decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); - } catch (FlacDecoderJni.FlacFrameDecodeException e) { - throw new IOException("Cannot read frame at position " + lastDecodePosition, e); - } - int outputSize = outputByteBuffer.limit(); - if (outputSize == 0) { - return RESULT_END_OF_INPUT; - } + decodeStreamMetadata(input); - writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp()); - return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput); + } + + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; + long lastDecodePosition = decoderJni.getDecodePosition(); + try { + decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition); + } catch (FlacDecoderJni.FlacFrameDecodeException e) { + throw new IOException("Cannot read frame at position " + lastDecodePosition, e); + } + int outputSize = outputByteBuffer.limit(); + if (outputSize == 0) { + return RESULT_END_OF_INPUT; + } + + outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput); + return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + } finally { + decoderJni.clearData(); + } } @Override public void seek(long position, long timeUs) { if (position == 0) { - readPastStreamInfo = false; + streamMetadataDecoded = false; } if (decoderJni != null) { decoderJni.reset(position); } - if (flacBinarySearchSeeker != null) { - flacBinarySearchSeeker.setSeekTargetUs(timeUs); + if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); } } @Override public void release() { - flacBinarySearchSeeker = null; + binarySearchSeeker = null; if (decoderJni != null) { decoderJni.release(); decoderJni = null; @@ -179,123 +186,141 @@ public final class FlacExtractor implements Extractor { } /** - * Peeks ID3 tag data (if present) at the beginning of the input. + * Peeks ID3 tag data at the beginning of the input. * - * @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not - * present in the input. + * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input. */ @Nullable private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { input.resetPeekPosition(); Id3Decoder.FramePredicate id3FramePredicate = - isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; + id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; return id3Peeker.peekId3Data(input, id3FramePredicate); } + @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) + private FlacDecoderJni initDecoderJni(ExtractorInput input) { + FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni); + decoderJni.setData(input); + return decoderJni; + } + + @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized. + @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded. + @SuppressWarnings({"contracts.postcondition.not.satisfied"}) + private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException { + if (streamMetadataDecoded) { + return; + } + + FlacStreamMetadata streamMetadata; + try { + streamMetadata = decoderJni.decodeStreamMetadata(); + } catch (IOException e) { + decoderJni.reset(/* newPosition= */ 0); + input.setRetryPosition(/* position= */ 0, e); + throw e; + } + + streamMetadataDecoded = true; + if (this.streamMetadata == null) { + this.streamMetadata = streamMetadata; + binarySearchSeeker = + outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); + Metadata metadata = id3MetadataDisabled ? null : id3Metadata; + if (streamMetadata.metadata != null) { + metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); + } + outputFormat(streamMetadata, metadata, trackOutput); + outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); + outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); + } + } + + @RequiresNonNull("binarySearchSeeker") + private int handlePendingSeek( + ExtractorInput input, + PositionHolder seekPosition, + ParsableByteArray outputBuffer, + OutputFrameHolder outputFrameHolder, + TrackOutput trackOutput) + throws InterruptedException, IOException { + int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); + ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; + if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { + outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput); + } + return seekResult; + } + /** * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. * * @return Whether the input begins with {@link #FLAC_SIGNATURE}. */ - private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException { + private static boolean peekFlacSignature(ExtractorInput input) + throws IOException, InterruptedException { byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, 0, FLAC_SIGNATURE.length); + input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); return Arrays.equals(header, FLAC_SIGNATURE); } - private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException { - if (readPastStreamInfo) { - return; - } - - FlacStreamInfo streamInfo = decodeStreamInfo(input); - readPastStreamInfo = true; - if (this.streamInfo == null) { - updateFlacStreamInfo(input, streamInfo); - } - } - - private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) { - this.streamInfo = streamInfo; - outputSeekMap(input, streamInfo); - outputFormat(streamInfo); - outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); - outputByteBuffer = ByteBuffer.wrap(outputBuffer.data); - outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer); - } - - private FlacStreamInfo decodeStreamInfo(ExtractorInput input) - throws InterruptedException, IOException { - try { - FlacStreamInfo streamInfo = decoderJni.decodeMetadata(); - if (streamInfo == null) { - throw new IOException("Metadata decoding failed"); - } - return streamInfo; - } catch (IOException e) { - decoderJni.reset(0); - input.setRetryPosition(0, e); - throw e; - } - } - - private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) { - boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1; - SeekMap seekMap = - hasSeekTable - ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) - : getSeekMapForNonSeekTableFlac(input, streamInfo); - extractorOutput.seekMap(seekMap); - } - - private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) { - long inputLength = input.getLength(); - if (inputLength != C.LENGTH_UNSET) { + /** + * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to + * handle seeks. + */ + @Nullable + private static FlacBinarySearchSeeker outputSeekMap( + FlacDecoderJni decoderJni, + FlacStreamMetadata streamMetadata, + long streamLength, + ExtractorOutput output) { + boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1; + FlacBinarySearchSeeker binarySearchSeeker = null; + SeekMap seekMap; + if (hasSeekTable) { + seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); + } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); - flacBinarySearchSeeker = - new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni); - return flacBinarySearchSeeker.getSeekMap(); - } else { // can't seek at all, because there's no SeekTable and the input length is unknown. - return new SeekMap.Unseekable(streamInfo.durationUs()); + binarySearchSeeker = + new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); + seekMap = binarySearchSeeker.getSeekMap(); + } else { + seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); } + output.seekMap(seekMap); + return binarySearchSeeker; } - private void outputFormat(FlacStreamInfo streamInfo) { + private static void outputFormat( + FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) { Format mediaFormat = Format.createAudioSampleFormat( /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - streamInfo.bitRate(), - streamInfo.maxDecodedFrameSize(), - streamInfo.channels, - streamInfo.sampleRate, - getPcmEncoding(streamInfo.bitsPerSample), + streamMetadata.bitRate(), + streamMetadata.maxDecodedFrameSize(), + streamMetadata.channels, + streamMetadata.sampleRate, + getPcmEncoding(streamMetadata.bitsPerSample), /* encoderDelay= */ 0, /* encoderPadding= */ 0, /* initializationData= */ null, /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null, - isId3MetadataDisabled ? null : id3Metadata); - trackOutput.format(mediaFormat); + metadata); + output.format(mediaFormat); } - private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition) - throws InterruptedException, IOException { - int seekResult = - flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); - ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; - if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { - writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs); - } - return seekResult; - } - - private void writeLastSampleToOutput(int size, long lastSampleTimestamp) { - outputBuffer.setPosition(0); - trackOutput.sampleData(outputBuffer, size); - trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + private static void outputSample( + ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) { + sampleData.setPosition(0); + output.sampleData(sampleData, size); + output.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null); } /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */ diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc index 298719d48d..d60a7cead2 100644 --- a/extensions/flac/src/main/jni/flac_jni.cc +++ b/extensions/flac/src/main/jni/flac_jni.cc @@ -14,9 +14,12 @@ * limitations under the License. */ -#include #include +#include + #include +#include + #include "include/flac_parser.h" #define LOG_TAG "flac_jni" @@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { return NULL; } + jclass arrayListClass = env->FindClass("java/util/ArrayList"); + jmethodID arrayListConstructor = + env->GetMethodID(arrayListClass, "", "()V"); + jobject commentList = env->NewObject(arrayListClass, arrayListConstructor); + jmethodID arrayListAddMethod = + env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + + if (context->parser->areVorbisCommentsValid()) { + std::vector vorbisComments = + context->parser->getVorbisComments(); + for (std::vector::const_iterator vorbisComment = + vorbisComments.begin(); + vorbisComment != vorbisComments.end(); ++vorbisComment) { + jstring commentString = env->NewStringUTF((*vorbisComment).c_str()); + env->CallBooleanMethod(commentList, arrayListAddMethod, commentString); + env->DeleteLocalRef(commentString); + } + } + + jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor); + bool picturesValid = context->parser->arePicturesValid(); + if (picturesValid) { + std::vector pictures = context->parser->getPictures(); + jclass pictureFrameClass = env->FindClass( + "com/google/android/exoplayer2/metadata/flac/PictureFrame"); + jmethodID pictureFrameConstructor = + env->GetMethodID(pictureFrameClass, "", + "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); + for (std::vector::const_iterator picture = pictures.begin(); + picture != pictures.end(); ++picture) { + jstring mimeType = env->NewStringUTF(picture->mimeType.c_str()); + jstring description = env->NewStringUTF(picture->description.c_str()); + jbyteArray pictureData = env->NewByteArray(picture->data.size()); + env->SetByteArrayRegion(pictureData, 0, picture->data.size(), + (signed char *)&picture->data[0]); + jobject pictureFrame = env->NewObject( + pictureFrameClass, pictureFrameConstructor, picture->type, mimeType, + description, picture->width, picture->height, picture->depth, + picture->colors, pictureData); + env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame); + env->DeleteLocalRef(mimeType); + env->DeleteLocalRef(description); + env->DeleteLocalRef(pictureData); + } + } + const FLAC__StreamMetadata_StreamInfo &streamInfo = context->parser->getStreamInfo(); - jclass cls = env->FindClass( + jclass flacStreamMetadataClass = env->FindClass( "com/google/android/exoplayer2/util/" - "FlacStreamInfo"); - jmethodID constructor = env->GetMethodID(cls, "", "(IIIIIIIJ)V"); + "FlacStreamMetadata"); + jmethodID flacStreamMetadataConstructor = + env->GetMethodID(flacStreamMetadataClass, "", + "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); - return env->NewObject(cls, constructor, streamInfo.min_blocksize, - streamInfo.max_blocksize, streamInfo.min_framesize, - streamInfo.max_framesize, streamInfo.sample_rate, - streamInfo.channels, streamInfo.bits_per_sample, - streamInfo.total_samples); + return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, + streamInfo.min_blocksize, streamInfo.max_blocksize, + streamInfo.min_framesize, streamInfo.max_framesize, + streamInfo.sample_rate, streamInfo.channels, + streamInfo.bits_per_sample, streamInfo.total_samples, + commentList, pictureFrames); } DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 83d3367415..830f3e2178 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { case FLAC__METADATA_TYPE_SEEKTABLE: mSeekTable = &metadata->data.seek_table; break; + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + if (!mVorbisCommentsValid) { + FLAC__StreamMetadata_VorbisComment vorbisComment = + metadata->data.vorbis_comment; + for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) { + FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry = + vorbisComment.comments[i]; + if (vorbisCommentEntry.entry != NULL) { + std::string comment( + reinterpret_cast(vorbisCommentEntry.entry), + vorbisCommentEntry.length); + mVorbisComments.push_back(comment); + } + } + mVorbisCommentsValid = true; + } else { + ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); + } + break; + case FLAC__METADATA_TYPE_PICTURE: { + const FLAC__StreamMetadata_Picture *parsedPicture = + &metadata->data.picture; + FlacPicture picture; + picture.mimeType.assign(std::string(parsedPicture->mime_type)); + picture.description.assign( + std::string((char *)parsedPicture->description)); + picture.data.assign(parsedPicture->data, + parsedPicture->data + parsedPicture->data_length); + picture.width = parsedPicture->width; + picture.height = parsedPicture->height; + picture.depth = parsedPicture->depth; + picture.colors = parsedPicture->colors; + picture.type = parsedPicture->type; + mPictures.push_back(picture); + mPicturesValid = true; + break; + } default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); break; @@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source) mCurrentPos(0LL), mEOF(false), mStreamInfoValid(false), + mVorbisCommentsValid(false), + mPicturesValid(false), mWriteRequested(false), mWriteCompleted(false), mWriteBuffer(NULL), @@ -266,6 +305,10 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_STREAMINFO); FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__METADATA_TYPE_SEEKTABLE); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_VORBIS_COMMENT); + FLAC__stream_decoder_set_metadata_respond(mDecoder, + FLAC__METADATA_TYPE_PICTURE); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( mDecoder, read_callback, seek_callback, tell_callback, length_callback, diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index cea7fbe33b..14ba9e8725 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -19,6 +19,10 @@ #include +#include +#include +#include + // libFLAC parser #include "FLAC/stream_decoder.h" @@ -26,6 +30,17 @@ typedef int status_t; +struct FlacPicture { + int type; + std::string mimeType; + std::string description; + FLAC__uint32 width; + FLAC__uint32 height; + FLAC__uint32 depth; + FLAC__uint32 colors; + std::vector data; +}; + class FLACParser { public: FLACParser(DataSource *source); @@ -44,6 +59,14 @@ class FLACParser { return mStreamInfo; } + bool areVorbisCommentsValid() const { return mVorbisCommentsValid; } + + std::vector getVorbisComments() { return mVorbisComments; } + + bool arePicturesValid() const { return mPicturesValid; } + + const std::vector &getPictures() const { return mPictures; } + int64_t getLastFrameTimestamp() const { return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); } @@ -71,6 +94,10 @@ class FLACParser { mEOF = false; if (newPosition == 0) { mStreamInfoValid = false; + mVorbisCommentsValid = false; + mPicturesValid = false; + mVorbisComments.clear(); + mPictures.clear(); FLAC__stream_decoder_reset(mDecoder); } else { FLAC__stream_decoder_flush(mDecoder); @@ -116,6 +143,14 @@ class FLACParser { const FLAC__StreamMetadata_SeekTable *mSeekTable; uint64_t firstFrameOffset; + // cached when the VORBIS_COMMENT metadata is parsed by libFLAC + std::vector mVorbisComments; + bool mVorbisCommentsValid; + + // cached when the PICTURE metadata is parsed by libFLAC + std::vector mPictures; + bool mPicturesValid; + // cached when a decoded PCM block is "written" by libFLAC parser bool mWriteRequested; bool mWriteCompleted; diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 50acd6c040..1031d6f4b7 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 2df9448d08..0ef9f281c9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -34,7 +34,7 @@ android { dependencies { api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index f70125ba38..a6f0c3966a 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -1,7 +1,11 @@ # ExoPlayer Firebase JobDispatcher extension # +**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.** + This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. +[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md +[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android ## Getting the extension ## @@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md - diff --git a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java index d79dead0d7..c8975275f1 100644 --- a/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util; * * @see GoogleApiAvailability + * @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link + * com.google.android.exoplayer2.scheduler.PlatformScheduler}. */ +@Deprecated public final class JobDispatcherScheduler implements Scheduler { private static final boolean DEBUG = false; diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index c6f5a216ce..ecaa78e25b 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -32,7 +32,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.leanback:leanback:1.0.0' } diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index db2e073c8a..68bd422185 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion api 'com.squareup.okhttp3:okhttp:3.12.1' } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 56acbdb7d3..28f7b05465 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -39,6 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index f8ec477b88..dbce33b923 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -150,6 +151,7 @@ import java.util.List; } @Override + @Nullable protected OpusDecoderException decode( DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 285be96388..2c2c8f6972 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; @@ -49,9 +50,8 @@ public final class OpusLibrary { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ + /** Returns the version of the underlying library if available, or null otherwise. */ + @Nullable public static String getVersion() { return isAvailable() ? opusGetVersion() : null; } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index ca734c3657..b74be659ee 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -33,7 +33,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 02b68b831d..92450f0381 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -39,7 +39,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test.ext:junit:' + androidXTestVersion diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 57e5481b55..0e13e82630 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import androidx.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; @@ -120,8 +121,9 @@ import java.nio.ByteBuffer; } @Override - protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, - boolean reset) { + @Nullable + protected VpxDecoderException decode( + VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 5a65fc56ff..db056d5110 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; @@ -49,9 +50,8 @@ public final class VpxLibrary { return LOADER.isAvailable(); } - /** - * Returns the version of the underlying library if available, or null otherwise. - */ + /** Returns the version of the underlying library if available, or null otherwise. */ + @Nullable public static String getVersion() { return isAvailable() ? vpxGetVersion() : null; } @@ -60,6 +60,7 @@ public final class VpxLibrary { * Returns the configuration string with which the underlying library was built if available, or * null otherwise. */ + @Nullable public static String getBuildConfig() { return isAvailable() ? vpxGetBuildConfig() : null; } diff --git a/extensions/workmanager/README.md b/extensions/workmanager/README.md new file mode 100644 index 0000000000..bd2dbc71ad --- /dev/null +++ b/extensions/workmanager/README.md @@ -0,0 +1,22 @@ +# ExoPlayer WorkManager extension + +This extension provides a Scheduler implementation which uses [WorkManager][]. + +[WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager.html + +## Getting the extension + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +implementation 'com.google.android.exoplayer:extension-workmanager:2.X.X' +``` + +where `2.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle new file mode 100644 index 0000000000..ea7564316f --- /dev/null +++ b/extensions/workmanager/build.gradle @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + testOptions.unitTests.includeAndroidResources = true +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation 'androidx.work:work-runtime:2.1.0' +} + +ext { + javadocTitle = 'WorkManager extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-workmanager' + releaseDescription = 'WorkManager extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/workmanager/src/main/AndroidManifest.xml b/extensions/workmanager/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1daf50bd00 --- /dev/null +++ b/extensions/workmanager/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java new file mode 100644 index 0000000000..01801c9897 --- /dev/null +++ b/extensions/workmanager/src/main/java/com/google/android/exoplayer2/ext/workmanager/WorkManagerScheduler.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 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.workmanager; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; + +/** A {@link Scheduler} that uses {@link WorkManager}. */ +public final class WorkManagerScheduler implements Scheduler { + + private static final boolean DEBUG = false; + private static final String TAG = "WorkManagerScheduler"; + private static final String KEY_SERVICE_ACTION = "service_action"; + private static final String KEY_SERVICE_PACKAGE = "service_package"; + private static final String KEY_REQUIREMENTS = "requirements"; + + private final String workName; + + /** + * @param workName A name for work scheduled by this instance. If the same name was used by a + * previous instance, anything scheduled by the previous instance will be canceled by this + * instance if {@link #schedule(Requirements, String, String)} or {@link #cancel()} are + * called. + */ + public WorkManagerScheduler(String workName) { + this.workName = workName; + } + + @Override + public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { + Constraints constraints = buildConstraints(requirements); + Data inputData = buildInputData(requirements, servicePackage, serviceAction); + OneTimeWorkRequest workRequest = buildWorkRequest(constraints, inputData); + logd("Scheduling work: " + workName); + WorkManager.getInstance().enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest); + return true; + } + + @Override + public boolean cancel() { + logd("Canceling work: " + workName); + WorkManager.getInstance().cancelUniqueWork(workName); + return true; + } + + private static Constraints buildConstraints(Requirements requirements) { + Constraints.Builder builder = new Constraints.Builder(); + + if (requirements.isUnmeteredNetworkRequired()) { + builder.setRequiredNetworkType(NetworkType.UNMETERED); + } else if (requirements.isNetworkRequired()) { + builder.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + builder.setRequiredNetworkType(NetworkType.NOT_REQUIRED); + } + + if (requirements.isChargingRequired()) { + builder.setRequiresCharging(true); + } + + if (requirements.isIdleRequired() && Util.SDK_INT >= 23) { + setRequiresDeviceIdle(builder); + } + + return builder.build(); + } + + @TargetApi(23) + private static void setRequiresDeviceIdle(Constraints.Builder builder) { + builder.setRequiresDeviceIdle(true); + } + + private static Data buildInputData( + Requirements requirements, String servicePackage, String serviceAction) { + Data.Builder builder = new Data.Builder(); + + builder.putInt(KEY_REQUIREMENTS, requirements.getRequirements()); + builder.putString(KEY_SERVICE_PACKAGE, servicePackage); + builder.putString(KEY_SERVICE_ACTION, serviceAction); + + return builder.build(); + } + + private static OneTimeWorkRequest buildWorkRequest(Constraints constraints, Data inputData) { + OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SchedulerWorker.class); + + builder.setConstraints(constraints); + builder.setInputData(inputData); + + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link Worker} that starts the target service if the requirements are met. */ + // This class needs to be public so that WorkManager can instantiate it. + public static final class SchedulerWorker extends Worker { + + private final WorkerParameters workerParams; + private final Context context; + + public SchedulerWorker(Context context, WorkerParameters workerParams) { + super(context, workerParams); + this.workerParams = workerParams; + this.context = context; + } + + @Override + public Result doWork() { + logd("SchedulerWorker is started"); + Data inputData = workerParams.getInputData(); + Assertions.checkNotNull(inputData, "Work started without input data."); + Requirements requirements = new Requirements(inputData.getInt(KEY_REQUIREMENTS, 0)); + if (requirements.checkRequirements(context)) { + logd("Requirements are met"); + String serviceAction = inputData.getString(KEY_SERVICE_ACTION); + String servicePackage = inputData.getString(KEY_SERVICE_PACKAGE); + Assertions.checkNotNull(serviceAction, "Service action missing."); + Assertions.checkNotNull(servicePackage, "Service package missing."); + Intent intent = new Intent(serviceAction).setPackage(servicePackage); + logd("Starting service action: " + serviceAction + " package: " + servicePackage); + Util.startForegroundService(context, intent); + return Result.success(); + } else { + logd("Requirements are not met"); + return Result.retry(); + } + } + } +} diff --git a/library/core/build.gradle b/library/core/build.gradle index 68ff8cc977..e633e12057 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -58,7 +58,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion 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 c004058082..a10416fac8 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 @@ -532,7 +532,9 @@ import java.util.concurrent.CopyOnWriteArrayList; public long getContentPosition() { if (isPlayingAd()) { playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); + return playbackInfo.contentPositionUs == C.TIME_UNSET + ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs() + : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); } else { return getCurrentPosition(); } 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 a9fe73371a..65a6866a9f 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 @@ -1304,8 +1304,11 @@ import java.util.concurrent.atomic.AtomicBoolean; Pair defaultPosition = getPeriodPosition( timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); - newContentPositionUs = defaultPosition.second; - newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, newContentPositionUs); + newPeriodId = queue.resolveMediaPeriodIdForAds(defaultPosition.first, defaultPosition.second); + if (!newPeriodId.isAd()) { + // Keep unset start position if we need to play an ad first. + newContentPositionUs = defaultPosition.second; + } } else if (timeline.getIndexOfPeriod(newPeriodId.periodUid) == C.INDEX_UNSET) { // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose // window we can restart from. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 190f4de5a6..f420f20767 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,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.10.3"; + public static final String VERSION = "2.10.4"; /** 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.10.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,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 = 2010003; + public static final int VERSION_INT = 2010004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index bc1ea7b1e1..2733df7ba6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -29,7 +29,8 @@ import com.google.android.exoplayer2.util.Util; public final long startPositionUs; /** * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} - * otherwise. + * if this is not an ad or the next content media period should be played from its default + * position. */ public final long contentPositionUs; /** 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 86fa5e11ee..2927d03114 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 @@ -144,7 +144,9 @@ import com.google.android.exoplayer2.util.Assertions; MediaPeriodInfo info) { long rendererPositionOffsetUs = loading == null - ? (info.id.isAd() ? info.contentPositionUs : 0) + ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET + ? info.contentPositionUs + : 0) : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder( @@ -560,6 +562,7 @@ import com.google.android.exoplayer2.util.Assertions; } long startPositionUs; + long contentPositionUs; int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; Object nextPeriodUid = period.uid; @@ -568,6 +571,7 @@ import com.google.android.exoplayer2.util.Assertions; // We're starting to buffer a new window. When playback transitions to this window we'll // want it to be from its default start position, so project the default start position // forward by the duration of the buffer, and start buffering from this point. + contentPositionUs = C.TIME_UNSET; Pair defaultPosition = timeline.getPeriodPosition( window, @@ -587,12 +591,13 @@ import com.google.android.exoplayer2.util.Assertions; windowSequenceNumber = nextWindowSequenceNumber++; } } else { + // We're starting to buffer a new period within the same window. startPositionUs = 0; + contentPositionUs = 0; } MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber); - return getMediaPeriodInfo( - periodId, /* contentPositionUs= */ startPositionUs, startPositionUs); + return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = mediaPeriodInfo.id; @@ -616,13 +621,11 @@ import com.google.android.exoplayer2.util.Assertions; mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } else { - // Play content from the ad group position. As a special case, if we're transitioning from a - // preroll ad group to content and there are no other ad groups, project the start position - // forward as if this were a transition to a new window. No attempt is made to handle - // midrolls in live streams, as it's unclear what content position should play after an ad - // (server-side dynamic ad insertion is more appropriate for this use case). + // Play content from the ad group position. long startPositionUs = mediaPeriodInfo.contentPositionUs; - if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) { + if (startPositionUs == C.TIME_UNSET) { + // If we're transitioning from an ad group to content starting from its default position, + // project the start position forward as if this were a transition to a new window. Pair defaultPosition = timeline.getPeriodPosition( window, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 0792bf0c7d..7107963c83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -48,7 +48,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * If {@link #periodId} refers to an ad, the position of the suspended content relative to the * start of the associated period in the {@link #timeline}, in microseconds. {@link C#TIME_UNSET} - * if {@link #periodId} does not refer to an ad. + * if {@link #periodId} does not refer to an ad or if the suspended content should be played from + * its default position. */ public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index ace7ebbcc6..07a1438519 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -364,8 +364,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return Collections.singletonList(passthroughDecoderInfo); } } - return mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + List decoderInfos = + mediaCodecSelector.getDecoderInfos( + format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) { + // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. + List eac3DecoderInfos = + mediaCodecSelector.getDecoderInfos( + MimeTypes.AUDIO_E_AC3, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos.addAll(eac3DecoderInfos); + } + return Collections.unmodifiableList(decoderInfos); } /** @@ -393,7 +402,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); codecNeedsEosBufferTimestampWorkaround = codecNeedsEosBufferTimestampWorkaround(codecInfo.name); passthroughEnabled = codecInfo.passthrough; - String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; + String codecMimeType = passthroughEnabled ? MimeTypes.AUDIO_RAW : codecInfo.codecMimeType; MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize, codecOperatingRate); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index 0bf6baa4d0..6cd46bb705 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -30,6 +30,7 @@ import java.util.Arrays; private static final int MINIMUM_PITCH = 65; private static final int MAXIMUM_PITCH = 400; private static final int AMDF_FREQUENCY = 4000; + private static final int BYTES_PER_SAMPLE = 2; private final int inputSampleRateHz; private final int channelCount; @@ -157,9 +158,9 @@ import java.util.Arrays; maxDiff = 0; } - /** Returns the number of output frames that can be read with {@link #getOutput(ShortBuffer)}. */ - public int getFramesAvailable() { - return outputFrameCount; + /** Returns the size of output that can be read with {@link #getOutput(ShortBuffer)}, in bytes. */ + public int getOutputSize() { + return outputFrameCount * channelCount * BYTES_PER_SAMPLE; } // Internal methods. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index 0d938d33f4..bd32e5ee6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -210,7 +210,7 @@ public final class SonicAudioProcessor implements AudioProcessor { sonic.queueInput(shortBuffer); inputBuffer.position(inputBuffer.position() + inputSize); } - int outputSize = sonic.getFramesAvailable() * channelCount * 2; + int outputSize = sonic.getOutputSize(); if (outputSize > 0) { if (buffer.capacity() < outputSize) { buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder()); @@ -243,7 +243,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public boolean isEnded() { - return inputEnded && (sonic == null || sonic.getFramesAvailable() == 0); + return inputEnded && (sonic == null || sonic.getOutputSize() == 0); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index f8204f6be3..b5650860e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -301,5 +301,6 @@ public abstract class SimpleDecoder< * @param reset Whether the decoder must be reset before decoding. * @return A decoder exception if an error occurred, or null if decoding was successful. */ - protected abstract @Nullable E decode(I inputBuffer, O outputBuffer, boolean reset); + @Nullable + protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index 87bb992082..e454bd51c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -186,10 +186,6 @@ public final class MpegAudioHeader { } } - // Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that - // seeking to a given timestamp and playing from the start up to that timestamp give the same - // results for CBR streams. See also [internal: b/120390268]. - bitrate = 8 * frameSize * sampleRate / samplesPerFrame; String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index d7a1c75302..0d5a168197 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -119,7 +119,7 @@ public interface TrackOutput { * Called to write sample data to the output. * * @param data A {@link ParsableByteArray} from which to read the sample data. - * @param length The number of bytes to read. + * @param length The number of bytes to read, starting from {@code data.getPosition()}. */ void sampleData(ParsableByteArray data, int length); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index eb1cc8f336..806cc9fad4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.extractor.flv; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,7 +46,7 @@ import java.util.Map; private long durationUs; public ScriptTagPayloadReader() { - super(null); + super(new DummyTrackOutput()); durationUs = C.TIME_UNSET; } @@ -138,7 +140,10 @@ import java.util.Map; ArrayList list = new ArrayList<>(count); for (int i = 0; i < count; i++) { int type = readAmfType(data); - list.add(readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + list.add(value); + } } return list; } @@ -157,7 +162,10 @@ import java.util.Map; if (type == AMF_TYPE_END_MARKER) { break; } - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -174,7 +182,10 @@ import java.util.Map; for (int i = 0; i < count; i++) { String key = readAmfString(data); int type = readAmfType(data); - array.put(key, readAmfData(data, type)); + Object value = readAmfData(data, type); + if (value != null) { + array.put(key, value); + } } return array; } @@ -191,6 +202,7 @@ import java.util.Map; return date; } + @Nullable private static Object readAmfData(ParsableByteArray data, int type) { switch (type) { case AMF_TYPE_NUMBER: @@ -208,8 +220,8 @@ import java.util.Map; case AMF_TYPE_DATE: return readAmfDate(data); default: + // We don't log a warning because there are types that we knowingly don't support. return null; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index c65ad0bc67..bc218e26ad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -117,6 +117,7 @@ public final class Mp3Extractor implements Extractor { private Seeker seeker; private long basisTimeUs; private long samplesRead; + private long firstSamplePosition; private int sampleBytesRemaining; public Mp3Extractor() { @@ -214,6 +215,13 @@ public final class Mp3Extractor implements Extractor { /* selectionFlags= */ 0, /* language= */ null, (flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata)); + firstSamplePosition = input.getPosition(); + } else if (firstSamplePosition != 0) { + long inputPosition = input.getPosition(); + if (inputPosition < firstSamplePosition) { + // Skip past the seek frame. + input.skipFully((int) (firstSamplePosition - inputPosition)); + } } return readSample(input); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 6fb0ac6856..70873825e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -1140,10 +1140,6 @@ import java.util.List; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); - } else if (childAtomType == Atom.TYPE_alac) { - initializationData = new byte[childAtomSize]; - parent.setPosition(childPosition); - parent.readBytes(initializationData, /* offset= */ 0, childAtomSize); } else if (childAtomType == Atom.TYPE_dOps) { // Build an Opus Identification Header (defined in RFC-7845) by concatenating the Opus Magic // Signature and the body of the dOps atom. @@ -1152,7 +1148,7 @@ import java.util.List; System.arraycopy(opusMagic, 0, initializationData, 0, opusMagic.length); parent.setPosition(childPosition + Atom.HEADER_SIZE); parent.readBytes(initializationData, opusMagic.length, childAtomBodySize); - } else if (childAtomSize == Atom.TYPE_dfLa) { + } else if (childAtomSize == Atom.TYPE_dfLa || childAtomType == Atom.TYPE_alac) { int childAtomBodySize = childAtomSize - Atom.FULL_HEADER_SIZE; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 9d3635e8b3..7676926c4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -123,6 +123,7 @@ public final class Track { * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no * such entry exists. */ + @Nullable public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { return sampleDescriptionEncryptionBoxes == null ? null : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index 5bd29c6e75..a35d211aa4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -52,7 +52,7 @@ public final class TrackEncryptionBox { * If {@link #perSampleIvSize} is 0, holds the default initialization vector as defined in the * track encryption box or sample group description box. Null otherwise. */ - public final byte[] defaultInitializationVector; + @Nullable public final byte[] defaultInitializationVector; /** * @param isEncrypted See {@link #isEncrypted}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java index c83662ee83..51ab94ba0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java @@ -16,29 +16,32 @@ package com.google.android.exoplayer2.extractor.ogg; import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; /** Seeks in an Ogg stream. */ /* package */ final class DefaultOggSeeker implements OggSeeker { - @VisibleForTesting public static final int MATCH_RANGE = 72000; - @VisibleForTesting public static final int MATCH_BYTE_RANGE = 100000; + private static final int MATCH_RANGE = 72000; + private static final int MATCH_BYTE_RANGE = 100000; private static final int DEFAULT_OFFSET = 30000; private static final int STATE_SEEK_TO_END = 0; private static final int STATE_READ_LAST_PAGE = 1; private static final int STATE_SEEK = 2; - private static final int STATE_IDLE = 3; + private static final int STATE_SKIP = 3; + private static final int STATE_IDLE = 4; private final OggPageHeader pageHeader = new OggPageHeader(); - private final long startPosition; - private final long endPosition; + private final long payloadStartPosition; + private final long payloadEndPosition; private final StreamReader streamReader; private int state; @@ -54,26 +57,27 @@ import java.io.IOException; /** * Constructs an OggSeeker. * - * @param startPosition Start position of the payload (inclusive). - * @param endPosition End position of the payload (exclusive). * @param streamReader The {@link StreamReader} that owns this seeker. + * @param payloadStartPosition Start position of the payload (inclusive). + * @param payloadEndPosition End position of the payload (exclusive). * @param firstPayloadPageSize The total size of the first payload page, in bytes. * @param firstPayloadPageGranulePosition The granule position of the first payload page. - * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the - * ogg stream. + * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page. */ public DefaultOggSeeker( - long startPosition, - long endPosition, StreamReader streamReader, + long payloadStartPosition, + long payloadEndPosition, long firstPayloadPageSize, long firstPayloadPageGranulePosition, boolean firstPayloadPageIsLastPage) { - Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); + Assertions.checkArgument( + payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition); this.streamReader = streamReader; - this.startPosition = startPosition; - this.endPosition = endPosition; - if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) { + this.payloadStartPosition = payloadStartPosition; + this.payloadEndPosition = payloadEndPosition; + if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition + || firstPayloadPageIsLastPage) { totalGranules = firstPayloadPageGranulePosition; state = STATE_IDLE; } else { @@ -90,7 +94,7 @@ import java.io.IOException; positionBeforeSeekToEnd = input.getPosition(); state = STATE_READ_LAST_PAGE; // Seek to the end just before the last page of stream to get the duration. - long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE; + long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE; if (lastPageSearchPosition > positionBeforeSeekToEnd) { return lastPageSearchPosition; } @@ -100,145 +104,110 @@ import java.io.IOException; state = STATE_IDLE; return positionBeforeSeekToEnd; case STATE_SEEK: - long currentGranule; - if (targetGranule == 0) { - currentGranule = 0; - } else { - long position = getNextSeekPosition(targetGranule, input); - if (position >= 0) { - return position; - } - currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2)); + long position = getNextSeekPosition(input); + if (position != C.POSITION_UNSET) { + return position; } + state = STATE_SKIP; + // Fall through. + case STATE_SKIP: + skipToPageOfTargetGranule(input); state = STATE_IDLE; - return -(currentGranule + 2); + return -(startGranule + 2); default: // Never happens. throw new IllegalStateException(); } } - @Override - public long startSeek(long timeUs) { - Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); - targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs); - state = STATE_SEEK; - resetSeeking(); - return targetGranule; - } - @Override public OggSeekMap createSeekMap() { return totalGranules != 0 ? new OggSeekMap() : null; } - @VisibleForTesting - public void resetSeeking() { - start = startPosition; - end = endPosition; + @Override + public void startSeek(long targetGranule) { + this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1); + state = STATE_SEEK; + start = payloadStartPosition; + end = payloadEndPosition; startGranule = 0; endGranule = totalGranules; } /** - * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} - * has to seek and then be passed for another call until a negative number is returned. If a - * negative number is returned the input is at a position which is before the target page and at - * which it is sensible to just skip pages to the target granule and pre-roll instead of doing - * another seek request. + * Performs a single step of a seeking binary search, returning the byte position from which data + * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged. + * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be + * called to skip to the target page. * - * @param targetGranule the target granule position to seek to. - * @param input the {@link ExtractorInput} to read from. - * @return the position to seek the {@link ExtractorInput} to for a next call or -(currentGranule - * + 2) if it's close enough to skip to the target page. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. + * @param input The {@link ExtractorInput} to read from. + * @return The byte position from which data should be provided for the next step, or {@link + * C#POSITION_UNSET} if the search has converged. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. */ - @VisibleForTesting - public long getNextSeekPosition(long targetGranule, ExtractorInput input) - throws IOException, InterruptedException { + private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException { if (start == end) { - return -(startGranule + 2); + return C.POSITION_UNSET; } - long initialPosition = input.getPosition(); + long currentPosition = input.getPosition(); if (!skipToNextPage(input, end)) { - if (start == initialPosition) { + if (start == currentPosition) { throw new IOException("No ogg page can be found."); } return start; } - pageHeader.populate(input, false); + pageHeader.populate(input, /* quiet= */ false); input.resetPeekPosition(); long granuleDistance = targetGranule - pageHeader.granulePosition; int pageSize = pageHeader.headerSize + pageHeader.bodySize; - if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) { - if (granuleDistance < 0) { - end = initialPosition; - endGranule = pageHeader.granulePosition; - } else { - start = input.getPosition() + pageSize; - startGranule = pageHeader.granulePosition; - if (end - start + pageSize < MATCH_BYTE_RANGE) { - input.skipFully(pageSize); - return -(startGranule + 2); - } - } - - if (end - start < MATCH_BYTE_RANGE) { - end = start; - return start; - } - - long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); - long nextPosition = input.getPosition() - offset - + (granuleDistance * (end - start) / (endGranule - startGranule)); - - nextPosition = Math.max(nextPosition, start); - nextPosition = Math.min(nextPosition, end - 1); - return nextPosition; + if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) { + return C.POSITION_UNSET; } - // position accepted (before target granule and within MATCH_RANGE) - input.skipFully(pageSize); - return -(pageHeader.granulePosition + 2); + if (granuleDistance < 0) { + end = currentPosition; + endGranule = pageHeader.granulePosition; + } else { + start = input.getPosition() + pageSize; + startGranule = pageHeader.granulePosition; + } + + if (end - start < MATCH_BYTE_RANGE) { + end = start; + return start; + } + + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); + long nextPosition = + input.getPosition() + - offset + + (granuleDistance * (end - start) / (endGranule - startGranule)); + return Util.constrainValue(nextPosition, start, end - 1); } - private long getEstimatedPosition(long position, long granuleDistance, long offset) { - position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset; - if (position < startPosition) { - position = startPosition; + /** + * Skips forward to the start of the page containing the {@code targetGranule}. + * + * @param input The {@link ExtractorInput} to read from. + * @throws ParserException If populating the page header fails. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + private void skipToPageOfTargetGranule(ExtractorInput input) + throws IOException, InterruptedException { + pageHeader.populate(input, /* quiet= */ false); + while (pageHeader.granulePosition <= targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + start = input.getPosition(); + startGranule = pageHeader.granulePosition; + pageHeader.populate(input, /* quiet= */ false); } - if (position >= endPosition) { - position = endPosition - 1; - } - return position; - } - - private class OggSeekMap implements SeekMap { - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - if (timeUs == 0) { - return new SeekPoints(new SeekPoint(0, startPosition)); - } - long granule = streamReader.convertTimeToGranule(timeUs); - long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); - return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); - } - - @Override - public long getDurationUs() { - return streamReader.convertGranuleToTime(totalGranules); - } - + input.resetPeekPosition(); } /** @@ -251,7 +220,7 @@ import java.io.IOException; */ @VisibleForTesting void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException { - if (!skipToNextPage(input, endPosition)) { + if (!skipToNextPage(input, payloadEndPosition)) { // Not found until eof. throw new EOFException(); } @@ -263,13 +232,12 @@ import java.io.IOException; * @param input The {@code ExtractorInput} to skip to the next page. * @param limit The limit up to which the search should take place. * @return Whether the next page was found. - * @throws IOException thrown if peeking/reading from the input fails. - * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + * @throws IOException If peeking/reading from the input fails. + * @throws InterruptedException If interrupted while peeking/reading from the input. */ - @VisibleForTesting - boolean skipToNextPage(ExtractorInput input, long limit) + private boolean skipToNextPage(ExtractorInput input, long limit) throws IOException, InterruptedException { - limit = Math.min(limit + 3, endPosition); + limit = Math.min(limit + 3, payloadEndPosition); byte[] buffer = new byte[2048]; int peekLength = buffer.length; while (true) { @@ -310,39 +278,35 @@ import java.io.IOException; long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException { skipToNextPage(input); pageHeader.reset(); - while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) { - pageHeader.populate(input, false); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) { + pageHeader.populate(input, /* quiet= */ false); input.skipFully(pageHeader.headerSize + pageHeader.bodySize); } return pageHeader.granulePosition; } - /** - * Skips to the position of the start of the page containing the {@code targetGranule} and returns - * the granule of the page previous to the target page. - * - * @param input the {@link ExtractorInput} to read from. - * @param targetGranule the target granule. - * @param currentGranule the current granule or -1 if it's unknown. - * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior - * page. - * @throws ParserException thrown if populating the page header fails. - * @throws IOException thrown if reading from the input fails. - * @throws InterruptedException thrown if interrupted while reading from the input. - */ - @VisibleForTesting - long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule) - throws IOException, InterruptedException { - pageHeader.populate(input, false); - while (pageHeader.granulePosition < targetGranule) { - input.skipFully(pageHeader.headerSize + pageHeader.bodySize); - // Store in a member field to be able to resume after IOExceptions. - currentGranule = pageHeader.granulePosition; - // Peek next header. - pageHeader.populate(input, false); - } - input.resetPeekPosition(); - return currentGranule; - } + private final class OggSeekMap implements SeekMap { + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + long targetGranule = streamReader.convertTimeToGranule(timeUs); + long estimatedPosition = + payloadStartPosition + + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules) + - DEFAULT_OFFSET; + estimatedPosition = + Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); + } + + @Override + public long getDurationUs() { + return streamReader.convertGranuleToTime(totalGranules); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 5eb0727908..4efd5c5e11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.FlacStreamInfo; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -38,7 +38,7 @@ import java.util.List; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; - private FlacStreamInfo streamInfo; + private FlacStreamMetadata streamMetadata; private FlacOggSeeker flacOggSeeker; public static boolean verifyBitstreamType(ParsableByteArray data) { @@ -50,7 +50,7 @@ import java.util.List; protected void reset(boolean headerData) { super.reset(headerData); if (headerData) { - streamInfo = null; + streamMetadata = null; flacOggSeeker = null; } } @@ -71,14 +71,24 @@ import java.util.List; protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) throws IOException, InterruptedException { byte[] data = packet.data; - if (streamInfo == null) { - streamInfo = new FlacStreamInfo(data, 17); + if (streamMetadata == null) { + streamMetadata = new FlacStreamMetadata(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks List initializationData = Collections.singletonList(metadata); - setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null, - Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate, - initializationData, null, 0, null); + setupData.format = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_FLAC, + null, + Format.NO_VALUE, + streamMetadata.bitRate(), + streamMetadata.channels, + streamMetadata.sampleRate, + initializationData, + null, + 0, + null); } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { flacOggSeeker = new FlacOggSeeker(); flacOggSeeker.parseSeekTable(packet); @@ -175,11 +185,9 @@ import java.util.List; } @Override - public long startSeek(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); + public void startSeek(long targetGranule) { + int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); pendingSeekGranule = seekPointGranules[index]; - return granule; } @Override @@ -211,7 +219,7 @@ import java.util.List; @Override public long getDurationUs() { - return streamInfo.durationUs(); + return streamMetadata.durationUs(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index bbf7e2fc6b..bb84909f67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -38,7 +38,13 @@ import java.io.IOException; public int revision; public int type; + /** + * The absolute granule position of the page. This is the total number of samples from the start + * of the file up to the end of the page. Samples partially in the page that continue on + * the next page do not count. + */ public long granulePosition; + public long streamSerialNumber; public long pageSequenceNumber; public long pageChecksum; @@ -72,10 +78,10 @@ import java.io.IOException; * Peeks an Ogg page header and updates this {@link OggPageHeader}. * * @param input The {@link ExtractorInput} to read from. - * @param quiet If {@code true}, no exceptions are thrown but {@code false} is returned if - * something goes wrong. - * @return {@code true} if the read was successful. The read fails if the end of the input is - * encountered without reading data. + * @param quiet Whether to return {@code false} rather than throwing an exception if the header + * cannot be populated. + * @return Whether the read was successful. The read fails if the end of the input is encountered + * without reading data. * @throws IOException If reading data fails or the stream is invalid. * @throws InterruptedException If the thread is interrupted. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java index aa88e5bf89..e4c3a163e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java @@ -33,16 +33,14 @@ import java.io.IOException; SeekMap createSeekMap(); /** - * Initializes a seek operation. + * Starts a seek operation. * - * @param timeUs The seek position in microseconds. - * @return The granule position targeted by the seek. + * @param targetGranule The target granule position. */ - long startSeek(long timeUs); + void startSeek(long targetGranule); /** - * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a - * progressive seek. + * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek. *

* If more data is required or if the position of the input needs to be modified then a position * from which data should be provided is returned. Else a negative value is returned. If a seek diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index e459ad1e58..d2671125e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -91,7 +91,8 @@ import java.io.IOException; reset(!seekMapSet); } else { if (state != STATE_READ_HEADERS) { - targetGranule = oggSeeker.startSeek(timeUs); + targetGranule = convertTimeToGranule(timeUs); + oggSeeker.startSeek(targetGranule); state = STATE_READ_PAYLOAD; } } @@ -147,9 +148,9 @@ import java.io.IOException; boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream. oggSeeker = new DefaultOggSeeker( + this, payloadStartPosition, input.getLength(), - this, firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize, firstPayloadPageHeader.granulePosition, isLastPage); @@ -248,13 +249,13 @@ import java.io.IOException; private static final class UnseekableOggSeeker implements OggSeeker { @Override - public long read(ExtractorInput input) throws IOException, InterruptedException { + public long read(ExtractorInput input) { return -1; } @Override - public long startSeek(long timeUs) { - return 0; + public void startSeek(long targetGranule) { + // Do nothing. } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 68d252e318..91097c9e5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -87,12 +87,14 @@ public final class WavExtractor implements Extractor { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); extractorOutput.seekMap(wavHeader); + } else if (input.getPosition() == 0) { + input.skipFully(wavHeader.getDataStartPosition()); } - long dataLimit = wavHeader.getDataLimit(); - Assertions.checkState(dataLimit != C.POSITION_UNSET); + long dataEndPosition = wavHeader.getDataEndPosition(); + Assertions.checkState(dataEndPosition != C.POSITION_UNSET); - long bytesLeft = dataLimit - input.getPosition(); + long bytesLeft = dataEndPosition - input.getPosition(); if (bytesLeft <= 0) { return Extractor.RESULT_END_OF_INPUT; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index c60117be60..6e3c5988a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -33,23 +33,29 @@ import com.google.android.exoplayer2.util.Util; private final int blockAlignment; /** Bits per sample for the audio data. */ private final int bitsPerSample; - /** The PCM encoding */ - @C.PcmEncoding - private final int encoding; + /** The PCM encoding. */ + @C.PcmEncoding private final int encoding; - /** Offset to the start of sample data. */ - private long dataStartPosition; - /** Total size in bytes of the sample data. */ - private long dataSize; + /** Position of the start of the sample data, in bytes. */ + private int dataStartPosition; + /** Position of the end of the sample data (exclusive), in bytes. */ + private long dataEndPosition; - public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment, - int bitsPerSample, @C.PcmEncoding int encoding) { + public WavHeader( + int numChannels, + int sampleRateHz, + int averageBytesPerSecond, + int blockAlignment, + int bitsPerSample, + @C.PcmEncoding int encoding) { this.numChannels = numChannels; this.sampleRateHz = sampleRateHz; this.averageBytesPerSecond = averageBytesPerSecond; this.blockAlignment = blockAlignment; this.bitsPerSample = bitsPerSample; this.encoding = encoding; + dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; } // Data bounds. @@ -57,22 +63,33 @@ import com.google.android.exoplayer2.util.Util; /** * Sets the data start position and size in bytes of sample data in this WAV. * - * @param dataStartPosition The data start position in bytes. - * @param dataSize The data size in bytes. + * @param dataStartPosition The position of the start of the sample data, in bytes. + * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. */ - public void setDataBounds(long dataStartPosition, long dataSize) { + public void setDataBounds(int dataStartPosition, long dataEndPosition) { this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; + this.dataEndPosition = dataEndPosition; } - /** Returns the data limit, or {@link C#POSITION_UNSET} if the data bounds have not been set. */ - public long getDataLimit() { - return hasDataBounds() ? (dataStartPosition + dataSize) : C.POSITION_UNSET; + /** + * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if + * the data bounds have not been set. + */ + public int getDataStartPosition() { + return dataStartPosition; + } + + /** + * Returns the position of the end of the sample data (exclusive), in bytes, or {@link + * C#POSITION_UNSET} if the data bounds have not been set. + */ + public long getDataEndPosition() { + return dataEndPosition; } /** Returns whether the data start position and size have been set. */ public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; + return dataStartPosition != C.POSITION_UNSET; } // SeekMap implementation. @@ -84,12 +101,13 @@ import com.google.android.exoplayer2.util.Util; @Override public long getDurationUs() { - long numFrames = dataSize / blockAlignment; + long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } @Override public SeekPoints getSeekPoints(long timeUs) { + long dataSize = dataEndPosition - dataStartPosition; long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / blockAlignment) * blockAlignment; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index c7b7a40ead..bbcb75aa2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ @@ -92,8 +91,8 @@ import java.io.IOException; // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... input.advancePeekPosition((int) chunkHeader.size - 16); - return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, - bitsPerSample, encoding); + return new WavHeader( + numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, bitsPerSample, encoding); } /** @@ -122,11 +121,13 @@ import java.io.IOException; ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); // Skip all chunks until we hit the data header. ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); - while (chunkHeader.id != Util.getIntegerCodeForString("data")) { - Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + while (chunkHeader.id != WavUtil.DATA_FOURCC) { + if (chunkHeader.id != WavUtil.RIFF_FOURCC && chunkHeader.id != WavUtil.FMT_FOURCC) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + } long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; // Override size of RIFF chunk, since it describes its size as the entire file. - if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { + if (chunkHeader.id == WavUtil.RIFF_FOURCC) { bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; } if (bytesToSkip > Integer.MAX_VALUE) { @@ -138,7 +139,14 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + int dataStartPosition = (int) input.getPosition(); + long dataEndPosition = dataStartPosition + chunkHeader.size; + long inputLength = input.getLength(); + if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { + Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); + dataEndPosition = inputLength; + } + wavHeader.setDataBounds(dataStartPosition, dataEndPosition); } private WavHeaderReader() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 2158f182b1..7fc748485b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -54,8 +54,15 @@ public final class MediaCodecInfo { public final @Nullable String mimeType; /** - * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if this - * is a passthrough codec. + * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this + * is a passthrough codec. Equal to {@link #mimeType} unless the codec is known to use a + * non-standard MIME type alias. + */ + @Nullable public final String codecMimeType; + + /** + * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not + * known. */ public final @Nullable CodecCapabilities capabilities; @@ -98,6 +105,7 @@ public final class MediaCodecInfo { return new MediaCodecInfo( name, /* mimeType= */ null, + /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, /* forceDisableAdaptive= */ false, @@ -109,26 +117,10 @@ public final class MediaCodecInfo { * * @param name The name of the {@link MediaCodec}. * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. - * @return The created instance. - */ - public static MediaCodecInfo newInstance(String name, String mimeType, - CodecCapabilities capabilities) { - return new MediaCodecInfo( - name, - mimeType, - capabilities, - /* passthrough= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - - /** - * Creates an instance. - * - * @param name The name of the {@link MediaCodec}. - * @param mimeType A mime type supported by the {@link MediaCodec}. - * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. + * @param codecMimeType The MIME type that the codec uses for media of type {@code #mimeType}. + * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. + * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or + * {@code null} if not known. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. @@ -136,22 +128,31 @@ public final class MediaCodecInfo { public static MediaCodecInfo newInstance( String name, String mimeType, - CodecCapabilities capabilities, + String codecMimeType, + @Nullable CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( - name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); + name, + mimeType, + codecMimeType, + capabilities, + /* passthrough= */ false, + forceDisableAdaptive, + forceSecure); } private MediaCodecInfo( String name, @Nullable String mimeType, + @Nullable String codecMimeType, @Nullable CodecCapabilities capabilities, boolean passthrough, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; + this.codecMimeType = codecMimeType; this.capabilities = capabilities; this.passthrough = passthrough; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); 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 c3072a1590..a8cf0f12e2 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 @@ -1806,9 +1806,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { String name = codecInfo.name; - return (Util.SDK_INT <= 17 - && ("OMX.rk.video_decoder.avc".equals(name) - || "OMX.allwinner.video.decoder.avc".equals(name))) + return (Util.SDK_INT <= 25 && "OMX.rk.video_decoder.avc".equals(name)) + || (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name)) || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 374c15eea0..a6391e4cc7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -161,24 +161,17 @@ public final class MediaCodecUtil { Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure, tunneling) : new MediaCodecListCompatV16(); - ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); } } - if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { - // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. - CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure, key.tunneling); - ArrayList eac3DecoderInfos = - getDecoderInfosInternal(eac3Key, mediaCodecList, MimeTypes.AUDIO_E_AC3); - decoderInfos.addAll(eac3DecoderInfos); - } applyWorkarounds(mimeType, decoderInfos); List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); decoderInfosCache.put(key, unmodifiableDecoderInfos); @@ -249,13 +242,11 @@ public final class MediaCodecUtil { * * @param key The codec key. * @param mediaCodecList The codec list. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. * @return The codec information for usable codecs matching the specified key. * @throws DecoderQueryException If there was an error querying the available decoders. */ private static ArrayList getDecoderInfosInternal(CodecKey key, - MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { + MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; @@ -265,28 +256,27 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String name = codecInfo.getName(); - String supportedType = - getCodecSupportedType(codecInfo, name, secureDecodersExplicit, requestedMimeType); - if (supportedType == null) { + String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (codecMimeType == null) { continue; } try { - CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(codecMimeType); boolean tunnelingSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); boolean tunnelingRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_TunneledPlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_TunneledPlayback, codecMimeType, capabilities); if ((!key.tunneling && tunnelingRequired) || (key.tunneling && !tunnelingSupported)) { continue; } boolean secureSupported = mediaCodecList.isFeatureSupported( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); boolean secureRequired = mediaCodecList.isFeatureRequired( - CodecCapabilities.FEATURE_SecurePlayback, supportedType, capabilities); + CodecCapabilities.FEATURE_SecurePlayback, codecMimeType, capabilities); if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { continue; } @@ -295,12 +285,18 @@ public final class MediaCodecUtil { || (!secureDecodersExplicit && !key.secure)) { decoderInfos.add( MediaCodecInfo.newInstance( - name, mimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ false)); + name, + mimeType, + codecMimeType, + capabilities, + forceDisableAdaptive, + /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { decoderInfos.add( MediaCodecInfo.newInstance( name + ".secure", mimeType, + codecMimeType, capabilities, forceDisableAdaptive, /* forceSecure= */ true)); @@ -314,7 +310,7 @@ public final class MediaCodecUtil { } else { // Rethrow error querying primary codec capabilities, or secondary codec // capabilities if API level is greater than 23. - Log.e(TAG, "Failed to query codec " + name + " (" + supportedType + ")"); + Log.e(TAG, "Failed to query codec " + name + " (" + codecMimeType + ")"); throw e; } } @@ -328,42 +324,49 @@ public final class MediaCodecUtil { } /** - * Returns the codec's supported type for decoding {@code requestedMimeType} on the current - * device, or {@code null} if the codec can't be used. + * Returns the codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. * * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. - * @return The codec's supported type for decoding {@code requestedMimeType}, or {@code null} if - * the codec can't be used. + * @param mimeType The MIME type. + * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if + * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} + * except in cases where the codec is known to use a non-standard MIME type alias. */ @Nullable - private static String getCodecSupportedType( + private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, boolean secureDecodersExplicit, - String requestedMimeType) { - if (isCodecUsableDecoder(info, name, secureDecodersExplicit, requestedMimeType)) { - if (requestedMimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { - // Handle decoders that declare support for DV via MIME types that aren't - // video/dolby-vision. - if ("OMX.MS.HEVCDV.Decoder".equals(name)) { - return "video/hevcdv"; - } else if ("OMX.RTK.video.decoder".equals(name) - || "OMX.realtek.video.decoder.tunneled".equals(name)) { - return "video/dv_hevc"; - } - } + String mimeType) { + if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { + return null; + } - String[] supportedTypes = info.getSupportedTypes(); - for (String supportedType : supportedTypes) { - if (supportedType.equalsIgnoreCase(requestedMimeType)) { - return supportedType; - } + String[] supportedTypes = info.getSupportedTypes(); + for (String supportedType : supportedTypes) { + if (supportedType.equalsIgnoreCase(mimeType)) { + return supportedType; } } + + if (mimeType.equals(MimeTypes.VIDEO_DOLBY_VISION)) { + // Handle decoders that declare support for DV via MIME types that aren't + // video/dolby-vision. + if ("OMX.MS.HEVCDV.Decoder".equals(name)) { + return "video/hevcdv"; + } else if ("OMX.RTK.video.decoder".equals(name) + || "OMX.realtek.video.decoder.tunneled".equals(name)) { + return "video/dv_hevc"; + } + } else if (mimeType.equals(MimeTypes.AUDIO_ALAC) && "OMX.lge.alac.decoder".equals(name)) { + return "audio/x-lg-alac"; + } else if (mimeType.equals(MimeTypes.AUDIO_FLAC) && "OMX.lge.flac.decoder".equals(name)) { + return "audio/x-lg-flac"; + } + return null; } @@ -373,12 +376,14 @@ public final class MediaCodecUtil { * @param info The codec information. * @param name The name of the codec * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. - * @param requestedMimeType The originally requested MIME type, which may differ from the codec - * key MIME type if the codec key is being considered as a fallback. + * @param mimeType The MIME type. * @return Whether the specified codec is usable for decoding on the current device. */ - private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String requestedMimeType) { + private static boolean isCodecUsableDecoder( + android.media.MediaCodecInfo info, + String name, + boolean secureDecodersExplicit, + String mimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -386,11 +391,11 @@ public final class MediaCodecUtil { // Work around broken audio decoders. if (Util.SDK_INT < 21 && ("CIPAACDecoder".equals(name) - || "CIPMP3Decoder".equals(name) - || "CIPVorbisDecoder".equals(name) - || "CIPAMRNBDecoder".equals(name) - || "AACDecoder".equals(name) - || "MP3Decoder".equals(name))) { + || "CIPMP3Decoder".equals(name) + || "CIPVorbisDecoder".equals(name) + || "CIPAMRNBDecoder".equals(name) + || "AACDecoder".equals(name) + || "MP3Decoder".equals(name))) { return false; } @@ -399,7 +404,7 @@ public final class MediaCodecUtil { if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && ("a70".equals(Util.DEVICE) - || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } @@ -408,17 +413,17 @@ public final class MediaCodecUtil { if (Util.SDK_INT == 16 && "OMX.qcom.audio.decoder.mp3".equals(name) && ("dlxu".equals(Util.DEVICE) // HTC Butterfly - || "protou".equals(Util.DEVICE) // HTC Desire X - || "ville".equals(Util.DEVICE) // HTC One S - || "villeplus".equals(Util.DEVICE) - || "villec2".equals(Util.DEVICE) - || Util.DEVICE.startsWith("gee") // LGE Optimus G - || "C6602".equals(Util.DEVICE) // Sony Xperia Z - || "C6603".equals(Util.DEVICE) - || "C6606".equals(Util.DEVICE) - || "C6616".equals(Util.DEVICE) - || "L36h".equals(Util.DEVICE) - || "SO-02E".equals(Util.DEVICE))) { + || "protou".equals(Util.DEVICE) // HTC Desire X + || "ville".equals(Util.DEVICE) // HTC One S + || "villeplus".equals(Util.DEVICE) + || "villec2".equals(Util.DEVICE) + || Util.DEVICE.startsWith("gee") // LGE Optimus G + || "C6602".equals(Util.DEVICE) // Sony Xperia Z + || "C6603".equals(Util.DEVICE) + || "C6606".equals(Util.DEVICE) + || "C6616".equals(Util.DEVICE) + || "L36h".equals(Util.DEVICE) + || "SO-02E".equals(Util.DEVICE))) { return false; } @@ -426,9 +431,9 @@ public final class MediaCodecUtil { if (Util.SDK_INT == 16 && "OMX.qcom.audio.decoder.aac".equals(name) && ("C1504".equals(Util.DEVICE) // Sony Xperia E - || "C1505".equals(Util.DEVICE) - || "C1604".equals(Util.DEVICE) // Sony Xperia E dual - || "C1605".equals(Util.DEVICE))) { + || "C1505".equals(Util.DEVICE) + || "C1604".equals(Util.DEVICE) // Sony Xperia E dual + || "C1605".equals(Util.DEVICE))) { return false; } @@ -437,13 +442,13 @@ public final class MediaCodecUtil { && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 - || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge - || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ - || "SC-05G".equals(Util.DEVICE) // Galaxy S6 - || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active - || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge - || "SC-04G".equals(Util.DEVICE) - || "SCV31".equals(Util.DEVICE))) { + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { return false; } @@ -453,10 +458,10 @@ public final class MediaCodecUtil { && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("d2") - || Util.DEVICE.startsWith("serrano") - || Util.DEVICE.startsWith("jflte") - || Util.DEVICE.startsWith("santos") - || Util.DEVICE.startsWith("t0"))) { + || Util.DEVICE.startsWith("serrano") + || Util.DEVICE.startsWith("jflte") + || Util.DEVICE.startsWith("santos") + || Util.DEVICE.startsWith("t0"))) { return false; } @@ -467,7 +472,7 @@ public final class MediaCodecUtil { } // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. - if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType) + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java new file mode 100644 index 0000000000..ce134614ad --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/PictureFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 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.metadata.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import java.util.Arrays; + +/** A picture parsed from a FLAC file. */ +public final class PictureFrame implements Metadata.Entry { + + /** The type of the picture. */ + public final int pictureType; + /** The mime type of the picture. */ + public final String mimeType; + /** A description of the picture. */ + public final String description; + /** The width of the picture in pixels. */ + public final int width; + /** The height of the picture in pixels. */ + public final int height; + /** The color depth of the picture in bits-per-pixel. */ + public final int depth; + /** For indexed-color pictures (e.g. GIF), the number of colors used. 0 otherwise. */ + public final int colors; + /** The encoded picture data. */ + public final byte[] pictureData; + + public PictureFrame( + int pictureType, + String mimeType, + String description, + int width, + int height, + int depth, + int colors, + byte[] pictureData) { + this.pictureType = pictureType; + this.mimeType = mimeType; + this.description = description; + this.width = width; + this.height = height; + this.depth = depth; + this.colors = colors; + this.pictureData = pictureData; + } + + /* package */ PictureFrame(Parcel in) { + this.pictureType = in.readInt(); + this.mimeType = castNonNull(in.readString()); + this.description = castNonNull(in.readString()); + this.width = in.readInt(); + this.height = in.readInt(); + this.depth = in.readInt(); + this.colors = in.readInt(); + this.pictureData = castNonNull(in.createByteArray()); + } + + @Override + public String toString() { + return "Picture: mimeType=" + mimeType + ", description=" + description; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PictureFrame other = (PictureFrame) obj; + return (pictureType == other.pictureType) + && mimeType.equals(other.mimeType) + && description.equals(other.description) + && (width == other.width) + && (height == other.height) + && (depth == other.depth) + && (colors == other.colors) + && Arrays.equals(pictureData, other.pictureData); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + pictureType; + result = 31 * result + mimeType.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + depth; + result = 31 * result + colors; + result = 31 * result + Arrays.hashCode(pictureData); + return result; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(pictureType); + dest.writeString(mimeType); + dest.writeString(description); + dest.writeInt(width); + dest.writeInt(height); + dest.writeInt(depth); + dest.writeInt(colors); + dest.writeByteArray(pictureData); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PictureFrame createFromParcel(Parcel in) { + return new PictureFrame(in); + } + + @Override + public PictureFrame[] newArray(int size) { + return new PictureFrame[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java new file mode 100644 index 0000000000..9f44cdf393 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/flac/VorbisComment.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2019 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.metadata.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; + +/** A vorbis comment. */ +public final class VorbisComment implements Metadata.Entry { + + /** The key. */ + public final String key; + + /** The value. */ + public final String value; + + /** + * @param key The key. + * @param value The value. + */ + public VorbisComment(String key, String value) { + this.key = key; + this.value = value; + } + + /* package */ VorbisComment(Parcel in) { + this.key = castNonNull(in.readString()); + this.value = castNonNull(in.readString()); + } + + @Override + public String toString() { + return "VC: " + key + "=" + value; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + VorbisComment other = (VorbisComment) obj; + return key.equals(other.key) && value.equals(other.value); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeString(value); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public VorbisComment createFromParcel(Parcel in) { + return new VorbisComment(in); + } + + @Override + public VorbisComment[] newArray(int size) { + return new VorbisComment[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 3900dc8e93..6587984f0c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -174,6 +174,7 @@ public abstract class DownloadService extends Service { @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final String channelId; @StringRes private final int channelNameResourceId; + @StringRes private final int channelDescriptionResourceId; private DownloadManager downloadManager; private int lastStartId; @@ -214,7 +215,23 @@ public abstract class DownloadService extends Service { foregroundNotificationId, foregroundNotificationUpdateInterval, /* channelId= */ null, - /* channelNameResourceId= */ 0); + /* channelNameResourceId= */ 0, + /* channelDescriptionResourceId= */ 0); + } + + /** @deprecated Use {@link #DownloadService(int, long, String, int, int)}. */ + @Deprecated + protected DownloadService( + int foregroundNotificationId, + long foregroundNotificationUpdateInterval, + @Nullable String channelId, + @StringRes int channelNameResourceId) { + this( + foregroundNotificationId, + foregroundNotificationUpdateInterval, + channelId, + channelNameResourceId, + /* channelDescriptionResourceId= */ 0); } /** @@ -230,25 +247,33 @@ public abstract class DownloadService extends Service { * unique per package. The value may be truncated if it's too long. Ignored if {@code * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. * @param channelNameResourceId A string resource identifier for the user visible name of the - * channel, if {@code channelId} is specified. The recommended maximum length is 40 - * characters. The value may be truncated if it is too long. Ignored if {@code + * notification channel. The recommended maximum length is 40 characters. The value may be + * truncated if it's too long. Ignored if {@code channelId} is null or if {@code * foregroundNotificationId} is {@link #FOREGROUND_NOTIFICATION_ID_NONE}. + * @param channelDescriptionResourceId A string resource identifier for the user visible + * description of the notification channel, or 0 if no description is provided. The + * recommended maximum length is 300 characters. The value may be truncated if it is too long. + * Ignored if {@code channelId} is null or if {@code foregroundNotificationId} is {@link + * #FOREGROUND_NOTIFICATION_ID_NONE}. */ protected DownloadService( int foregroundNotificationId, long foregroundNotificationUpdateInterval, @Nullable String channelId, - @StringRes int channelNameResourceId) { + @StringRes int channelNameResourceId, + @StringRes int channelDescriptionResourceId) { if (foregroundNotificationId == FOREGROUND_NOTIFICATION_ID_NONE) { this.foregroundNotificationUpdater = null; this.channelId = null; this.channelNameResourceId = 0; + this.channelDescriptionResourceId = 0; } else { this.foregroundNotificationUpdater = new ForegroundNotificationUpdater( foregroundNotificationId, foregroundNotificationUpdateInterval); this.channelId = channelId; this.channelNameResourceId = channelNameResourceId; + this.channelDescriptionResourceId = channelDescriptionResourceId; } } @@ -543,7 +568,11 @@ public abstract class DownloadService extends Service { public void onCreate() { if (channelId != null) { NotificationUtil.createNotificationChannel( - this, channelId, channelNameResourceId, NotificationUtil.IMPORTANCE_LOW); + this, + channelId, + channelNameResourceId, + channelDescriptionResourceId, + NotificationUtil.IMPORTANCE_LOW); } Class clazz = getClass(); DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index b40bbb35d1..c84847f755 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -106,13 +106,16 @@ public interface MediaPeriod extends SequenceableLoader { * Performs a track selection. * *

The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} - * indicating whether the existing {@code SampleStream} can be retained for each selection, and + * indicating whether the existing {@link SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * + *

Note that previously received {@link TrackSelection TrackSelections} are no longer valid and + * references need to be replaced even if the corresponding {@link SampleStream} is kept. + * *

This method is only called after the period has been prepared. * * @param selections The renderer track selections. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index b03dd0ea7c..72095c2c54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -118,6 +118,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @NullableType SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -144,6 +145,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public long seekToUs(long positionUs) { + positionUs = constrainSeekPosition(positionUs); for (int i = 0; i < sampleStreams.size(); i++) { ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs); } @@ -152,7 +154,7 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return positionUs; + return constrainSeekPosition(positionUs); } @Override @@ -172,6 +174,10 @@ public final class SilenceMediaSource extends BaseMediaSource { @Override public void reevaluateBuffer(long positionUs) {} + + private long constrainSeekPosition(long positionUs) { + return Util.constrainValue(positionUs, 0, durationUs); + } } private static final class SilenceSampleStream implements SampleStream { @@ -187,7 +193,7 @@ public final class SilenceMediaSource extends BaseMediaSource { } public void seekTo(long positionUs) { - positionBytes = getAudioByteCount(positionUs); + positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 4b54b3ea9a..3f6ff44248 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -28,9 +28,10 @@ import java.lang.annotation.RetentionPolicy; */ public class Cue { - /** - * An unset position or width. - */ + /** The empty cue. */ + public static final Cue EMPTY = new Cue(""); + + /** An unset position or width. */ public static final float DIMEN_UNSET = Float.MIN_VALUE; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index 38d6ff25cb..bd561afaf8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -69,6 +70,7 @@ public abstract class SimpleSubtitleDecoder extends @SuppressWarnings("ByteBufferBackingArray") @Override + @Nullable protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 75b7a01673..843cfab045 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; +import com.google.android.exoplayer2.util.Assertions; import java.util.List; /** @@ -45,22 +46,22 @@ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subti @Override public int getEventTimeCount() { - return subtitle.getEventTimeCount(); + return Assertions.checkNotNull(subtitle).getEventTimeCount(); } @Override public long getEventTime(int index) { - return subtitle.getEventTime(index) + subsampleOffsetUs; + return Assertions.checkNotNull(subtitle).getEventTime(index) + subsampleOffsetUs; } @Override public int getNextEventTimeIndex(long timeUs) { - return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getNextEventTimeIndex(timeUs - subsampleOffsetUs); } @Override public List getCues(long timeUs) { - return subtitle.getCues(timeUs - subsampleOffsetUs); + return Assertions.checkNotNull(subtitle).getCues(timeUs - subsampleOffsetUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 091bda49f3..9ef3556c8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.pgs; import android.graphics.Bitmap; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.Subtitle; @@ -41,7 +42,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private final ParsableByteArray inflatedBuffer; private final CueBuilder cueBuilder; - private Inflater inflater; + @Nullable private Inflater inflater; public PgsDecoder() { super("PgsDecoder"); @@ -76,6 +77,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { } } + @Nullable private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); @@ -197,6 +199,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { bitmapY = buffer.readUnsignedShort(); } + @Nullable public Cue build() { if (planeWidth == 0 || planeHeight == 0 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index c25b26128c..b1af75f613 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.ssa; +import androidx.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; @@ -49,7 +50,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { private int formatTextIndex; public SsaDecoder() { - this(null); + this(/* initializationData= */ null); } /** @@ -58,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * format line. The second must contain an SSA header that will be assumed common to all * samples. */ - public SsaDecoder(List initializationData) { + public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; @@ -201,7 +202,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { cues.add(new Cue(text)); cueTimesUs.add(startTimeUs); if (endTimeUs != C.TIME_UNSET) { - cues.add(null); + cues.add(Cue.EMPTY); cueTimesUs.add(endTimeUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 339119ed6b..9a3756194f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 6f9fd366ec..5dfaecee1d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -111,11 +111,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the text and tags. textBuilder.setLength(0); tags.clear(); - while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { + currentLine = subripData.readLine(); + while (!TextUtils.isEmpty(currentLine)) { if (textBuilder.length() > 0) { textBuilder.append("
"); } textBuilder.append(processLine(currentLine, tags)); + currentLine = subripData.readLine(); } Spanned text = Html.fromHtml(textBuilder.toString()); @@ -132,7 +134,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { cues.add(buildCue(text, alignmentTag)); if (haveEndTimecode) { - cues.add(null); + cues.add(Cue.EMPTY); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java index a79df478e5..01ed1711a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java @@ -32,7 +32,7 @@ import java.util.List; private final long[] cueTimesUs; /** - * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { @@ -61,7 +61,7 @@ import java.util.List; @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == null) { + if (index == -1 || cues[index] == Cue.EMPTY) { // timeUs is earlier than the start of the first cue, or we have an empty cue. return Collections.emptyList(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 9211dc51ce..ddc7a8f5f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -65,6 +65,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final float DEFAULT_VERTICAL_PLACEMENT = 0.85f; private final ParsableByteArray parsableByteArray; + private boolean customVerticalPlacement; private int defaultFontFace; private int defaultColorRgba; @@ -80,10 +81,7 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { public Tx3gDecoder(List initializationData) { super("Tx3gDecoder"); parsableByteArray = new ParsableByteArray(); - decodeInitializationData(initializationData); - } - private void decodeInitializationData(List initializationData) { if (initializationData != null && initializationData.size() == 1 && (initializationData.get(0).length == 48 || initializationData.get(0).length == 53)) { byte[] initializationBytes = initializationData.get(0); @@ -151,8 +149,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { } parsableByteArray.setPosition(position + atomSize); } - return new Tx3gSubtitle(new Cue(cueText, null, verticalPlacement, Cue.LINE_TYPE_FRACTION, - Cue.ANCHOR_TYPE_START, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET)); + return new Tx3gSubtitle( + new Cue( + cueText, + /* textAlignment= */ null, + verticalPlacement, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_START, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET)); } private static String readSubtitleText(ParsableByteArray parsableByteArray) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 949bd178ea..b8dd40f8bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -2318,14 +2318,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(format.language, language)) { return 3; } - // Partial match where one language is a subset of the other (e.g. "zho-hans" and "zho-hans-hk") + // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") if (format.language.startsWith(language) || language.startsWith(format.language)) { return 2; } - // Partial match where only the main language tag is the same (e.g. "fra-fr" and "fra-ca") - if (format.language.length() >= 3 - && language.length() >= 3 - && format.language.substring(0, 3).equals(language.substring(0, 3))) { + // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca") + String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0]; + String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; + if (formatMainLanguage.equals(queryMainLanguage)) { return 1; } return 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index de4a75d607..94a6e21c86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import android.util.Base64; @@ -29,9 +31,10 @@ public final class DataSchemeDataSource extends BaseDataSource { public static final String SCHEME_DATA = "data"; - private @Nullable DataSpec dataSpec; - private int bytesRead; - private @Nullable byte[] data; + @Nullable private DataSpec dataSpec; + @Nullable private byte[] data; + private int endPosition; + private int readPosition; public DataSchemeDataSource() { super(/* isNetwork= */ false); @@ -41,6 +44,7 @@ public final class DataSchemeDataSource extends BaseDataSource { public long open(DataSpec dataSpec) throws IOException { transferInitializing(dataSpec); this.dataSpec = dataSpec; + readPosition = (int) dataSpec.position; Uri uri = dataSpec.uri; String scheme = uri.getScheme(); if (!SCHEME_DATA.equals(scheme)) { @@ -61,8 +65,14 @@ public final class DataSchemeDataSource extends BaseDataSource { // TODO: Add support for other charsets. data = Util.getUtf8Bytes(URLDecoder.decode(dataString, C.ASCII_NAME)); } + endPosition = + dataSpec.length != C.LENGTH_UNSET ? (int) dataSpec.length + readPosition : data.length; + if (endPosition > data.length || readPosition > endPosition) { + data = null; + throw new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE); + } transferStarted(dataSpec); - return data.length; + return (long) endPosition - readPosition; } @Override @@ -70,29 +80,29 @@ public final class DataSchemeDataSource extends BaseDataSource { if (readLength == 0) { return 0; } - int remainingBytes = data.length - bytesRead; + int remainingBytes = endPosition - readPosition; if (remainingBytes == 0) { return C.RESULT_END_OF_INPUT; } readLength = Math.min(readLength, remainingBytes); - System.arraycopy(data, bytesRead, buffer, offset, readLength); - bytesRead += readLength; + System.arraycopy(castNonNull(data), readPosition, buffer, offset, readLength); + readPosition += readLength; bytesTransferred(readLength); return readLength; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSpec != null ? dataSpec.uri : null; } @Override - public void close() throws IOException { + public void close() { if (data != null) { data = null; transferEnded(); } dataSpec = null; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java similarity index 64% rename from library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 0df39e103d..2c814294af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -15,12 +15,18 @@ */ package com.google.android.exoplayer2.util; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import java.util.ArrayList; +import java.util.List; -/** - * Holder for FLAC stream info. - */ -public final class FlacStreamInfo { +/** Holder for FLAC metadata. */ +public final class FlacStreamMetadata { + + private static final String TAG = "FlacStreamMetadata"; public final int minBlockSize; public final int maxBlockSize; @@ -30,16 +36,19 @@ public final class FlacStreamInfo { public final int channels; public final int bitsPerSample; public final long totalSamples; + @Nullable public final Metadata metadata; + + private static final String SEPARATOR = "="; /** - * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. + * Parses binary FLAC stream info metadata. * - * @param data An array holding FLAC stream info metadata structure - * @param offset Offset of the structure in the array + * @param data An array containing binary FLAC stream info metadata. + * @param offset The offset of the stream info metadata in {@code data}. * @see FLAC format * METADATA_BLOCK_STREAMINFO */ - public FlacStreamInfo(byte[] data, int offset) { + public FlacStreamMetadata(byte[] data, int offset) { ParsableBitArray scratch = new ParsableBitArray(data); scratch.setPosition(offset * 8); this.minBlockSize = scratch.readBits(16); @@ -49,14 +58,11 @@ public final class FlacStreamInfo { this.sampleRate = scratch.readBits(20); this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; - this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) - | (scratch.readBits(32) & 0xFFFFFFFFL); - // Remaining 16 bytes is md5 value + this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); + this.metadata = null; } /** - * Constructs a FlacStreamInfo given the parameters. - * * @param minBlockSize Minimum block size of the FLAC stream. * @param maxBlockSize Maximum block size of the FLAC stream. * @param minFrameSize Minimum frame size of the FLAC stream. @@ -65,10 +71,16 @@ public final class FlacStreamInfo { * @param channels Number of channels of the FLAC stream. * @param bitsPerSample Number of bits per sample of the FLAC stream. * @param totalSamples Total samples of the FLAC stream. + * @param vorbisComments Vorbis comments. Each entry must be in key=value form. + * @param pictureFrames Picture frames. * @see FLAC format * METADATA_BLOCK_STREAMINFO + * @see FLAC format + * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE */ - public FlacStreamInfo( + public FlacStreamMetadata( int minBlockSize, int maxBlockSize, int minFrameSize, @@ -76,7 +88,9 @@ public final class FlacStreamInfo { int sampleRate, int channels, int bitsPerSample, - long totalSamples) { + long totalSamples, + List vorbisComments, + List pictureFrames) { this.minBlockSize = minBlockSize; this.maxBlockSize = maxBlockSize; this.minFrameSize = minFrameSize; @@ -85,6 +99,7 @@ public final class FlacStreamInfo { this.channels = channels; this.bitsPerSample = bitsPerSample; this.totalSamples = totalSamples; + this.metadata = buildMetadata(vorbisComments, pictureFrames); } /** Returns the maximum size for a decoded frame from the FLAC stream. */ @@ -126,4 +141,27 @@ public final class FlacStreamInfo { } return approxBytesPerFrame; } + + @Nullable + private static Metadata buildMetadata( + List vorbisComments, List pictureFrames) { + if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) { + return null; + } + + ArrayList metadataEntries = new ArrayList<>(); + for (int i = 0; i < vorbisComments.size(); i++) { + String vorbisComment = vorbisComments.get(i); + String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); + if (keyAndValue.length != 2) { + Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment); + } else { + VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); + metadataEntries.add(entry); + } + } + metadataEntries.addAll(pictureFrames); + + return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java index 4cd03f566d..756494f9d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NotificationUtil.java @@ -61,6 +61,14 @@ public final class NotificationUtil { /** @see NotificationManager#IMPORTANCE_HIGH */ public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; + /** @deprecated Use {@link #createNotificationChannel(Context, String, int, int, int)}. */ + @Deprecated + public static void createNotificationChannel( + Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + createNotificationChannel( + context, id, nameResourceId, /* descriptionResourceId= */ 0, importance); + } + /** * Creates a notification channel that notifications can be posted to. See {@link * NotificationChannel} and {@link @@ -70,21 +78,33 @@ public final class NotificationUtil { * @param id The id of the channel. Must be unique per package. The value may be truncated if it's * too long. * @param nameResourceId A string resource identifier for the user visible name of the channel. - * You can rename this channel when the system locale changes by listening for the {@link - * Intent#ACTION_LOCALE_CHANGED} broadcast. The recommended maximum length is 40 characters. - * The value may be truncated if it is too long. + * The recommended maximum length is 40 characters. The string may be truncated if it's too + * long. You can rename the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. + * @param descriptionResourceId A string resource identifier for the user visible description of + * the channel, or 0 if no description is provided. The recommended maximum length is 300 + * characters. The value may be truncated if it is too long. You can change the description of + * the channel when the system locale changes by listening for the {@link + * Intent#ACTION_LOCALE_CHANGED} broadcast. * @param importance The importance of the channel. This controls how interruptive notifications * posted to this channel are. One of {@link #IMPORTANCE_UNSPECIFIED}, {@link * #IMPORTANCE_NONE}, {@link #IMPORTANCE_MIN}, {@link #IMPORTANCE_LOW}, {@link * #IMPORTANCE_DEFAULT} and {@link #IMPORTANCE_HIGH}. */ public static void createNotificationChannel( - Context context, String id, @StringRes int nameResourceId, @Importance int importance) { + Context context, + String id, + @StringRes int nameResourceId, + @StringRes int descriptionResourceId, + @Importance int importance) { if (Util.SDK_INT >= 26) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel(id, context.getString(nameResourceId), importance); + if (descriptionResourceId != 0) { + channel.setDescription(context.getString(descriptionResourceId)); + } notificationManager.createNotificationChannel(channel); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 86ad6fd6b3..095394b2f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -71,6 +71,7 @@ import java.util.Calendar; import java.util.Collections; import java.util.Formatter; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; @@ -135,6 +136,10 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); + // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter + // ISO 639-2 code back to the corresponding 2-letter code. + @Nullable private static HashMap languageTagIso3ToIso2; + private Util() {} /** @@ -450,18 +455,31 @@ public final class Util { if (language == null) { return null; } - try { - Locale locale = getLocaleForLanguageTag(language); - int localeLanguageLength = locale.getLanguage().length(); - String normLanguage = locale.getISO3Language(); - if (normLanguage.isEmpty()) { - return toLowerInvariant(language); - } - String normTag = getLocaleLanguageTag(locale); - return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength)); - } catch (MissingResourceException e) { - return toLowerInvariant(language); + // Locale data (especially for API < 21) may produce tags with '_' instead of the + // standard-conformant '-'. + String normalizedTag = language.replace('_', '-'); + if (Util.SDK_INT >= 21) { + // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags. + normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag); } + if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { + // Tag isn't valid, keep using the original. + normalizedTag = language; + } + normalizedTag = Util.toLowerInvariant(normalizedTag); + String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; + if (mainLanguage.length() == 3) { + // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO + // 639-1 codes automatically. + if (languageTagIso3ToIso2 == null) { + languageTagIso3ToIso2 = createIso3ToIso2Map(); + } + String iso2Language = languageTagIso3ToIso2.get(mainLanguage); + if (iso2Language != null) { + normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); + } + } + return normalizedTag; } /** @@ -1955,32 +1973,25 @@ public final class Util { } private static String[] getSystemLocales() { + Configuration config = Resources.getSystem().getConfiguration(); return SDK_INT >= 24 - ? getSystemLocalesV24() - : new String[] {getLocaleLanguageTag(Resources.getSystem().getConfiguration().locale)}; + ? getSystemLocalesV24(config) + : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()}; } @TargetApi(24) - private static String[] getSystemLocalesV24() { - return Util.split(Resources.getSystem().getConfiguration().getLocales().toLanguageTags(), ","); - } - - private static Locale getLocaleForLanguageTag(String languageTag) { - return Util.SDK_INT >= 21 ? getLocaleForLanguageTagV21(languageTag) : new Locale(languageTag); + private static String[] getSystemLocalesV24(Configuration config) { + return Util.split(config.getLocales().toLanguageTags(), ","); } @TargetApi(21) - private static Locale getLocaleForLanguageTagV21(String languageTag) { - return Locale.forLanguageTag(languageTag); - } - - private static String getLocaleLanguageTag(Locale locale) { - return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString(); + private static String[] getSystemLocaleV21(Configuration config) { + return new String[] {config.locale.toLanguageTag()}; } @TargetApi(21) - private static String getLocaleLanguageTagV21(Locale locale) { - return locale.toLanguageTag(); + private static String normalizeLanguageCodeSyntaxV21(String languageTag) { + return Locale.forLanguageTag(languageTag).toLanguageTag(); } private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { @@ -2013,6 +2024,54 @@ public final class Util { } } + private static HashMap createIso3ToIso2Map() { + String[] iso2Languages = Locale.getISOLanguages(); + HashMap iso3ToIso2 = + new HashMap<>( + /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length); + for (String iso2 : iso2Languages) { + try { + // This returns the ISO 639-2/T code for the language. + String iso3 = new Locale(iso2).getISO3Language(); + if (!TextUtils.isEmpty(iso3)) { + iso3ToIso2.put(iso3, iso2); + } + } catch (MissingResourceException e) { + // Shouldn't happen for list of known languages, but we don't want to throw either. + } + } + // Add additional ISO 639-2/B codes to mapping. + for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) { + iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]); + } + return iso3ToIso2; + } + + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + private static final String[] iso3BibliographicalToIso2 = + new String[] { + "alb", "sq", + "arm", "hy", + "baq", "eu", + "bur", "my", + "tib", "bo", + "chi", "zh", + "cze", "cs", + "dut", "nl", + "ger", "de", + "gre", "el", + "fre", "fr", + "geo", "ka", + "ice", "is", + "mac", "mk", + "mao", "mi", + "may", "ms", + "per", "fa", + "rum", "ro", + "slo", "sk", + "wel", "cy" + }; + /** * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order * "most significant bit first". diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 8d5b890c7f..b5a935c15f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -551,10 +551,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format format, MediaCrypto crypto, float codecOperatingRate) { + String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); MediaFormat mediaFormat = getMediaFormat( format, + codecMimeType, codecMaxValues, codecOperatingRate, deviceNeedsNoPostProcessWorkaround, @@ -1111,6 +1113,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * * @param format The format of media. + * @param codecMimeType The MIME type handled by the codec. * @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. @@ -1123,13 +1126,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat( Format format, + String codecMimeType, CodecMaxValues codecMaxValues, float codecOperatingRate, boolean deviceNeedsNoPostProcessWorkaround, int tunnelingAudioSessionId) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. - mediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); + mediaFormat.setString(MediaFormat.KEY_MIME, codecMimeType); mediaFormat.setInteger(MediaFormat.KEY_WIDTH, format.width); mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, format.height); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); @@ -1429,6 +1433,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "1713": case "1714": case "A10-70F": + case "A10-70L": case "A1601": case "A2016a40": case "A7000-a": diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump index d4df3ffeba..96b0cd259c 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26122 + duration = 26125 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: 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 a715289a04..2203b34e86 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 @@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import android.content.Context; import android.graphics.SurfaceTexture; +import android.net.Uri; import androidx.annotation.Nullable; import android.view.Surface; import androidx.test.core.app.ApplicationProvider; @@ -2608,6 +2609,56 @@ public final class ExoPlayerTest { assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs)); } + @Test + public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception { + AdPlaybackState adPlaybackState = + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2")) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3")); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10_000_000, + adPlaybackState)); + final FakeMediaSource fakeMediaSource = new FakeMediaSource(fakeTimeline, null); + AtomicReference playerReference = new AtomicReference<>(); + AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET); + EventListener eventListener = + new EventListener() { + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + contentStartPositionMs.set(playerReference.get().getContentPosition()); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("contentWithInitialSeekAfterPrerollAd") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .seek(5_000) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(fakeMediaSource) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(contentStartPositionMs.get()).isAtLeast(5_000L); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index 8d1818845d..8ba0be26a0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -16,13 +16,16 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; import java.io.IOException; import java.util.Random; import org.junit.Test; @@ -32,13 +35,15 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DefaultOggSeekerTest { + private final Random random = new Random(0); + @Test public void testSetupWithUnsetEndPositionFails() { try { new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ C.LENGTH_UNSET, /* streamReader= */ new TestStreamReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ C.LENGTH_UNSET, /* firstPayloadPageSize= */ 1, /* firstPayloadPageGranulePosition= */ 1, /* firstPayloadPageIsLastPage= */ false); @@ -56,17 +61,106 @@ public final class DefaultOggSeekerTest { } } + @Test + public void testSkipToNextPage() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(4000, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(4000); + } + + @Test + public void testSkipToNextPageOverlap() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(2046, random), + new byte[] {'O', 'g', 'g', 'S'}, + TestUtil.buildTestData(4000, random)), + false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(2046); + } + + @Test + public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput( + TestUtil.joinByteArrays(new byte[] {'x', 'O', 'g', 'g', 'S'}), false); + skipToNextPage(extractorInput); + assertThat(extractorInput.getPosition()).isEqualTo(1); + } + + @Test + public void testSkipToNextPageNoMatch() throws Exception { + FakeExtractorInput extractorInput = + OggTestData.createInput(new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); + try { + skipToNextPage(extractorInput); + fail(); + } catch (EOFException e) { + // expected + } + } + + @Test + public void testReadGranuleOfLastPage() throws IOException, InterruptedException { + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + TestUtil.buildTestData(100, random), + OggTestData.buildOggHeader(0x00, 20000, 66, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x00, 40000, 67, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random), + OggTestData.buildOggHeader(0x05, 60000, 68, 3), + TestUtil.createByteArray(254, 254, 254), // laces + TestUtil.buildTestData(3 * 254, random)), + false); + assertReadGranuleOfLastPage(input, 60000); + } + + @Test + public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (EOFException e) { + // Ignored. + } + } + + @Test + public void testReadGranuleOfLastPageWithUnboundedLength() + throws IOException, InterruptedException { + FakeExtractorInput input = OggTestData.createInput(new byte[0], true); + try { + assertReadGranuleOfLastPage(input, 60000); + fail(); + } catch (IllegalArgumentException e) { + // Ignored. + } + } + private void testSeeking(Random random) throws IOException, InterruptedException { OggTestFile testFile = OggTestFile.generate(random, 1000); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); TestStreamReader streamReader = new TestStreamReader(); DefaultOggSeeker oggSeeker = new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ testFile.data.length, /* streamReader= */ streamReader, + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ testFile.data.length, /* firstPayloadPageSize= */ testFile.firstPayloadPageSize, - /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition, + /* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranuleCount, /* firstPayloadPageIsLastPage= */ false); OggPageHeader pageHeader = new OggPageHeader(); @@ -78,89 +172,96 @@ public final class DefaultOggSeekerTest { input.setPosition((int) nextSeekPosition); } - // Test granule 0 from file start - assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0); + // Test granule 0 from file start. + long granule = seekTo(input, oggSeeker, 0, 0); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - // Test granule 0 from file end - assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0); + // Test granule 0 from file end. + granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1); + assertThat(granule).isEqualTo(0); assertThat(input.getPosition()).isEqualTo(0); - { // Test last granule - long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0); - long position = testFile.data.length; - assertThat( - (testFile.lastGranule > currentGranule && position > input.getPosition()) - || (testFile.lastGranule == currentGranule && position == input.getPosition())) - .isTrue(); - } - - { // Test exact granule - input.setPosition(testFile.data.length / 2); - oggSeeker.skipToNextPage(input); - assertThat(pageHeader.populate(input, true)).isTrue(); - long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize; - long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0); - assertThat( - (pageHeader.granulePosition > currentGranule && position > input.getPosition()) - || (pageHeader.granulePosition == currentGranule - && position == input.getPosition())) - .isTrue(); - } + // Test last granule. + granule = seekTo(input, oggSeeker, testFile.granuleCount - 1, 0); + assertThat(granule).isEqualTo(testFile.granuleCount - testFile.lastPayloadPageGranuleCount); + assertThat(input.getPosition()).isEqualTo(testFile.data.length - testFile.lastPayloadPageSize); for (int i = 0; i < 100; i += 1) { - long targetGranule = (long) (random.nextDouble() * testFile.lastGranule); + long targetGranule = random.nextInt(testFile.granuleCount); int initialPosition = random.nextInt(testFile.data.length); - - long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition); + granule = seekTo(input, oggSeeker, targetGranule, initialPosition); long currentPosition = input.getPosition(); - - assertWithMessage("getNextSeekPosition() didn't leave input on a page start.") - .that(pageHeader.populate(input, true)) - .isTrue(); - - if (currentGranule == 0) { + if (granule == 0) { assertThat(currentPosition).isEqualTo(0); } else { int previousPageStart = testFile.findPreviousPageStart(currentPosition); input.setPosition(previousPageStart); - assertThat(pageHeader.populate(input, true)).isTrue(); - assertThat(currentGranule).isEqualTo(pageHeader.granulePosition); + pageHeader.populate(input, false); + assertThat(granule).isEqualTo(pageHeader.granulePosition); } input.setPosition((int) currentPosition); - oggSeeker.skipToPageOfGranule(input, targetGranule, -1); - long positionDiff = Math.abs(input.getPosition() - currentPosition); + pageHeader.populate(input, false); + // The target granule should be within the current page. + assertThat(granule).isAtMost(targetGranule); + assertThat(targetGranule).isLessThan(pageHeader.granulePosition); + } + } - long granuleDiff = currentGranule - targetGranule; - if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) - && positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { - fail( - "granuleDiff (" - + granuleDiff - + ") or positionDiff (" - + positionDiff - + ") is more than allowed."); + private static void skipToNextPage(ExtractorInput extractorInput) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ extractorInput.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + oggSeeker.skipToNextPage(extractorInput); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + /* ignored */ } } } - private long seekTo( + private static void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) + throws IOException, InterruptedException { + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + /* streamReader= */ new FlacReader(), + /* payloadStartPosition= */ 0, + /* payloadEndPosition= */ input.getLength(), + /* firstPayloadPageSize= */ 1, + /* firstPayloadPageGranulePosition= */ 2, + /* firstPayloadPageIsLastPage= */ false); + while (true) { + try { + assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); + break; + } catch (FakeExtractorInput.SimulatedIOException e) { + // Ignored. + } + } + } + + private static long seekTo( FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) throws IOException, InterruptedException { long nextSeekPosition = initialPosition; + oggSeeker.startSeek(targetGranule); int count = 0; - oggSeeker.resetSeeking(); - - do { - input.setPosition((int) nextSeekPosition); - nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input); - + while (nextSeekPosition >= 0) { if (count++ > 100) { - fail("infinite loop?"); + fail("Seek failed to converge in 100 iterations"); } - } while (nextSeekPosition >= 0); - + input.setPosition((int) nextSeekPosition); + nextSeekPosition = oggSeeker.read(input); + } return -(nextSeekPosition + 2); } @@ -171,8 +272,7 @@ public final class DefaultOggSeekerTest { } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { return false; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java deleted file mode 100644 index d6691f50f8..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor.ogg; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.OggTestData; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.EOFException; -import java.io.IOException; -import java.util.Random; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultOggSeeker} utility methods. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultOggSeekerUtilMethodsTest { - - private final Random random = new Random(0); - - @Test - public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(4000, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(4000); - } - - @Test - public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - TestUtil.buildTestData(2046, random), - new byte[] {'O', 'g', 'g', 'S'}, - TestUtil.buildTestData(4000, random) - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(2046); - } - - @Test - public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - TestUtil.joinByteArrays( - new byte[] {'x', 'O', 'g', 'g', 'S'} - ), false); - skipToNextPage(extractorInput); - assertThat(extractorInput.getPosition()).isEqualTo(1); - } - - @Test - public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = OggTestData.createInput( - new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); - try { - skipToNextPage(extractorInput); - fail(); - } catch (EOFException e) { - // expected - } - } - - private static void skipToNextPage(ExtractorInput extractorInput) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ extractorInput.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - oggSeeker.skipToNextPage(extractorInput); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ } - } - } - - @Test - public void testSkipToPageOfGranule() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - // expect to be granule of the previous page returned as elapsedSamples - skipToPageOfGranule(input, 54000, 40000); - // expect to be at the start of the third page - assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254))); - } - - @Test - public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 40000, 20000); - // expect to be at the start of the second page - assertThat(input.getPosition()).isEqualTo(30 + (3 * 254)); - } - - @Test - public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { - byte[] packet = TestUtil.buildTestData(3 * 254, random); - byte[] data = TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet, - OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), - TestUtil.createByteArray(254, 254, 254), // Laces. - packet); - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); - - skipToPageOfGranule(input, 10000, -1); - assertThat(input.getPosition()).isEqualTo(0); - } - - private void skipToPageOfGranule(ExtractorInput input, long granule, - long elapsedSamplesExpected) throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1)) - .isEqualTo(elapsedSamplesExpected); - return; - } catch (FakeExtractorInput.SimulatedIOException e) { - input.resetPeekPosition(); - } - } - } - - @Test - public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( - TestUtil.buildTestData(100, random), - OggTestData.buildOggHeader(0x00, 20000, 66, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x00, 40000, 67, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random), - OggTestData.buildOggHeader(0x05, 60000, 68, 3), - TestUtil.createByteArray(254, 254, 254), // laces - TestUtil.buildTestData(3 * 254, random) - ), false); - assertReadGranuleOfLastPage(input, 60000); - } - - @Test - public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (EOFException e) { - // ignored - } - } - - @Test - public void testReadGranuleOfLastPageWithUnboundedLength() - throws IOException, InterruptedException { - FakeExtractorInput input = OggTestData.createInput(new byte[0], true); - try { - assertReadGranuleOfLastPage(input, 60000); - fail(); - } catch (IllegalArgumentException e) { - // ignored - } - } - - private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) - throws IOException, InterruptedException { - DefaultOggSeeker oggSeeker = - new DefaultOggSeeker( - /* startPosition= */ 0, - /* endPosition= */ input.getLength(), - /* streamReader= */ new FlacReader(), - /* firstPayloadPageSize= */ 1, - /* firstPayloadPageGranulePosition= */ 2, - /* firstPayloadPageIsLastPage= */ false); - while (true) { - try { - assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); - break; - } catch (FakeExtractorInput.SimulatedIOException e) { - // ignored - } - } - } - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index e5512dda36..38e4332b16 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -30,35 +30,39 @@ import java.util.Random; private static final int MAX_GRANULES_IN_PAGE = 100000; public final byte[] data; - public final long lastGranule; - public final int packetCount; + public final int granuleCount; public final int pageCount; public final int firstPayloadPageSize; - public final long firstPayloadPageGranulePosition; + public final int firstPayloadPageGranuleCount; + public final int lastPayloadPageSize; + public final int lastPayloadPageGranuleCount; private OggTestFile( byte[] data, - long lastGranule, - int packetCount, + int granuleCount, int pageCount, int firstPayloadPageSize, - long firstPayloadPageGranulePosition) { + int firstPayloadPageGranuleCount, + int lastPayloadPageSize, + int lastPayloadPageGranuleCount) { this.data = data; - this.lastGranule = lastGranule; - this.packetCount = packetCount; + this.granuleCount = granuleCount; this.pageCount = pageCount; this.firstPayloadPageSize = firstPayloadPageSize; - this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition; + this.firstPayloadPageGranuleCount = firstPayloadPageGranuleCount; + this.lastPayloadPageSize = lastPayloadPageSize; + this.lastPayloadPageGranuleCount = lastPayloadPageGranuleCount; } public static OggTestFile generate(Random random, int pageCount) { ArrayList fileData = new ArrayList<>(); int fileSize = 0; - long granule = 0; - int packetLength = -1; - int packetCount = 0; + int granuleCount = 0; int firstPayloadPageSize = 0; - long firstPayloadPageGranulePosition = 0; + int firstPayloadPageGranuleCount = 0; + int lastPageloadPageSize = 0; + int lastPayloadPageGranuleCount = 0; + int packetLength = -1; for (int i = 0; i < pageCount; i++) { int headerType = 0x00; @@ -71,17 +75,17 @@ import java.util.Random; if (i == pageCount - 1) { headerType |= 4; } - granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; + int pageGranuleCount = random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); + granuleCount += pageGranuleCount; + byte[] header = OggTestData.buildOggHeader(headerType, granuleCount, 0, pageSegmentCount); fileData.add(header); - fileSize += header.length; + int pageSize = header.length; byte[] laces = new byte[pageSegmentCount]; int bodySize = 0; for (int j = 0; j < pageSegmentCount; j++) { if (packetLength < 0) { - packetCount++; if (i < pageCount - 1) { packetLength = random.nextInt(MAX_PACKET_LENGTH); } else { @@ -96,14 +100,19 @@ import java.util.Random; packetLength -= 255; } fileData.add(laces); - fileSize += laces.length; + pageSize += laces.length; byte[] payload = TestUtil.buildTestData(bodySize, random); fileData.add(payload); - fileSize += payload.length; + pageSize += payload.length; + + fileSize += pageSize; if (i == 0) { - firstPayloadPageSize = header.length + bodySize; - firstPayloadPageGranulePosition = granule; + firstPayloadPageSize = pageSize; + firstPayloadPageGranuleCount = pageGranuleCount; + } else if (i == pageCount - 1) { + lastPageloadPageSize = pageSize; + lastPayloadPageGranuleCount = pageGranuleCount; } } @@ -115,11 +124,12 @@ import java.util.Random; } return new OggTestFile( file, - granule, - packetCount, + granuleCount, pageCount, firstPayloadPageSize, - firstPayloadPageGranulePosition); + firstPayloadPageGranuleCount, + lastPageloadPageSize, + lastPayloadPageGranuleCount); } public int findPreviousPageStart(long position) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java new file mode 100644 index 0000000000..3f07dbc26d --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/PictureFrameTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.metadata.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link PictureFrame}. */ +@RunWith(AndroidJUnit4.class) +public final class PictureFrameTest { + + @Test + public void testParcelable() { + PictureFrame pictureFrameToParcel = new PictureFrame(0, "", "", 0, 0, 0, 0, new byte[0]); + + Parcel parcel = Parcel.obtain(); + pictureFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + PictureFrame pictureFrameFromParcel = PictureFrame.CREATOR.createFromParcel(parcel); + assertThat(pictureFrameFromParcel).isEqualTo(pictureFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java new file mode 100644 index 0000000000..bb118e381a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/flac/VorbisCommentTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.metadata.flac; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Test for {@link VorbisComment}. */ +@RunWith(AndroidJUnit4.class) +public final class VorbisCommentTest { + + @Test + public void testParcelable() { + VorbisComment vorbisCommentFrameToParcel = new VorbisComment("key", "value"); + + Parcel parcel = Parcel.obtain(); + vorbisCommentFrameToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + VorbisComment vorbisCommentFrameFromParcel = VorbisComment.CREATOR.createFromParcel(parcel); + assertThat(vorbisCommentFrameFromParcel).isEqualTo(vorbisCommentFrameToParcel); + + parcel.recycle(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 2df9a608e9..8cb142f05d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import org.junit.Before; @@ -31,6 +32,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class DataSchemeDataSourceTest { + private static final String DATA_SCHEME_URI = + "data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiLCJjb250ZW50X2lkIjoiTWpBeE5WOTBaV" + + "0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiXX0="; private DataSource schemeDataDataSource; @Before @@ -40,9 +44,7 @@ public final class DataSchemeDataSourceTest { @Test public void testBase64Data() throws IOException { - DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" - + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" - + "DAwMDAwMDAwMDAiXX0="); + DataSpec dataSpec = buildDataSpec(DATA_SCHEME_URI); DataSourceAsserts.assertDataSourceContent( schemeDataDataSource, dataSpec, @@ -72,6 +74,52 @@ public final class DataSchemeDataSourceTest { assertThat(Util.fromUtf8Bytes(buffer, 0, 18)).isEqualTo("012345678901234567"); } + @Test + public void testSequentialRangeRequests() throws IOException { + DataSpec dataSpec = + buildDataSpec(DATA_SCHEME_URI, /* position= */ 1, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 10, /* length= */ C.LENGTH_UNSET); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, + dataSpec, + Util.getUtf8Bytes( + "\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}")); + dataSpec = buildDataSpec(DATA_SCHEME_URI, /* position= */ 15, /* length= */ 5); + DataSourceAsserts.assertDataSourceContent( + schemeDataDataSource, dataSpec, Util.getUtf8Bytes("devin")); + } + + @Test + public void testInvalidStartPositionRequest() throws IOException { + try { + // Try to open a range starting one byte beyond the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 108, /* length= */ C.LENGTH_UNSET)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + + @Test + public void testRangeExceedingResourceLengthRequest() throws IOException { + try { + // Try to open a range exceeding the resource's length. + schemeDataDataSource.open( + buildDataSpec(DATA_SCHEME_URI, /* position= */ 97, /* length= */ 11)); + fail(); + } catch (DataSourceException e) { + assertThat(e.reason).isEqualTo(DataSourceException.POSITION_OUT_OF_RANGE); + } + } + @Test public void testIncorrectScheme() { try { @@ -99,7 +147,11 @@ public final class DataSchemeDataSourceTest { } private static DataSpec buildDataSpec(String uriString) { - return new DataSpec(Uri.parse(uriString)); + return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET); + } + + private static DataSpec buildDataSpec(String uriString, int position, int length) { + return new DataSpec(Uri.parse(uriString), position, length, /* key= */ null); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java index 0392f8b26d..2a1c59e7df 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -28,7 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for ColorParser. */ +/** Unit test for {@link ColorParser}. */ @RunWith(AndroidJUnit4.class) public final class ColorParserTest { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java new file mode 100644 index 0000000000..72a80161f2 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/FlacStreamMetadataTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link FlacStreamMetadata}. */ +@RunWith(AndroidJUnit4.class) +public final class FlacStreamMetadataTest { + + @Test + public void parseVorbisComments() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("Title=Song"); + commentsList.add("Artist=Singer"); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata.length()).isEqualTo(2); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("Song"); + commentFrame = (VorbisComment) metadata.get(1); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Singer"); + } + + @Test + public void parseEmptyVorbisComments() { + ArrayList commentsList = new ArrayList<>(); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata).isNull(); + } + + @Test + public void parseVorbisCommentWithEqualsInValue() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("Title=So=ng"); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata.length()).isEqualTo(1); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Title"); + assertThat(commentFrame.value).isEqualTo("So=ng"); + } + + @Test + public void parseInvalidVorbisComment() { + ArrayList commentsList = new ArrayList<>(); + commentsList.add("TitleSong"); + commentsList.add("Artist=Singer"); + + Metadata metadata = + new FlacStreamMetadata(0, 0, 0, 0, 0, 0, 0, 0, commentsList, new ArrayList<>()).metadata; + + assertThat(metadata.length()).isEqualTo(1); + VorbisComment commentFrame = (VorbisComment) metadata.get(0); + assertThat(commentFrame.key).isEqualTo("Artist"); + assertThat(commentFrame.value).isEqualTo("Singer"); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 9abec0cd8f..5a13ed0dd8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -268,14 +268,19 @@ public class UtilTest { @Test @Config(sdk = 21) public void testNormalizeLanguageCodeV21() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("spa-ar"); - assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("spa-ar-dialect"); - assertThat(Util.normalizeLanguageCode("es-419")).isEqualTo("spa-419"); - assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zho-hans-tw"); - assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zho-tw"); + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } @@ -283,13 +288,49 @@ public class UtilTest { @Test @Config(sdk = 16) public void testNormalizeLanguageCode() { - assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa"); - assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa"); + assertThat(Util.normalizeLanguageCode(null)).isNull(); + assertThat(Util.normalizeLanguageCode("")).isEmpty(); + assertThat(Util.normalizeLanguageCode("es")).isEqualTo("es"); + assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("es"); assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es_AR")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("spa_ar")).isEqualTo("es-ar"); + assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("es-ar-dialect"); + assertThat(Util.normalizeLanguageCode("ES-419")).isEqualTo("es-419"); + assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zh-hans-tw"); + // Doesn't work on API < 21 because we can't use Locale syntax verification. + // assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zh-tw"); + assertThat(Util.normalizeLanguageCode("zho-hans-tw")).isEqualTo("zh-hans-tw"); assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und"); assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist"); } + @Test + public void testNormalizeIso6392BibliographicalAndTextualCodes() { + // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. + assertThat(Util.normalizeLanguageCode("alb")).isEqualTo(Util.normalizeLanguageCode("sqi")); + assertThat(Util.normalizeLanguageCode("arm")).isEqualTo(Util.normalizeLanguageCode("hye")); + assertThat(Util.normalizeLanguageCode("baq")).isEqualTo(Util.normalizeLanguageCode("eus")); + assertThat(Util.normalizeLanguageCode("bur")).isEqualTo(Util.normalizeLanguageCode("mya")); + assertThat(Util.normalizeLanguageCode("chi")).isEqualTo(Util.normalizeLanguageCode("zho")); + assertThat(Util.normalizeLanguageCode("cze")).isEqualTo(Util.normalizeLanguageCode("ces")); + assertThat(Util.normalizeLanguageCode("dut")).isEqualTo(Util.normalizeLanguageCode("nld")); + assertThat(Util.normalizeLanguageCode("fre")).isEqualTo(Util.normalizeLanguageCode("fra")); + assertThat(Util.normalizeLanguageCode("geo")).isEqualTo(Util.normalizeLanguageCode("kat")); + assertThat(Util.normalizeLanguageCode("ger")).isEqualTo(Util.normalizeLanguageCode("deu")); + assertThat(Util.normalizeLanguageCode("gre")).isEqualTo(Util.normalizeLanguageCode("ell")); + assertThat(Util.normalizeLanguageCode("ice")).isEqualTo(Util.normalizeLanguageCode("isl")); + assertThat(Util.normalizeLanguageCode("mac")).isEqualTo(Util.normalizeLanguageCode("mkd")); + assertThat(Util.normalizeLanguageCode("mao")).isEqualTo(Util.normalizeLanguageCode("mri")); + assertThat(Util.normalizeLanguageCode("may")).isEqualTo(Util.normalizeLanguageCode("msa")); + assertThat(Util.normalizeLanguageCode("per")).isEqualTo(Util.normalizeLanguageCode("fas")); + assertThat(Util.normalizeLanguageCode("rum")).isEqualTo(Util.normalizeLanguageCode("ron")); + assertThat(Util.normalizeLanguageCode("slo")).isEqualTo(Util.normalizeLanguageCode("slk")); + assertThat(Util.normalizeLanguageCode("tib")).isEqualTo(Util.normalizeLanguageCode("bod")); + assertThat(Util.normalizeLanguageCode("wel")).isEqualTo(Util.normalizeLanguageCode("cym")); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); diff --git a/library/dash/build.gradle b/library/dash/build.gradle index f6981a2220..9f5775d478 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 40d4e468bd..f7edf62182 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -69,4 +69,11 @@ public interface DashChunkSource extends ChunkSource { * @param newManifest The new manifest. */ void updateManifest(DashManifest newManifest, int periodIndex); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 431a0a4bd9..8635005bfc 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -402,17 +402,27 @@ import java.util.regex.Pattern; int[] streamIndexToTrackGroupIndex) { // Create newly selected primary and event streams. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + if (streams[i] == null) { + // Create new stream for selection. streamResetFlags[i] = true; int trackGroupIndex = streamIndexToTrackGroupIndex[i]; TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + streams[i] = buildSampleStream(trackGroupInfo, selection, positionUs); } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); + Format format = selection.getTrackGroup().getFormat(0); streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); } + } else if (streams[i] instanceof ChunkSampleStream) { + // Update selection in existing stream. + @SuppressWarnings("unchecked") + ChunkSampleStream stream = (ChunkSampleStream) streams[i]; + stream.getChunkSource().updateTrackSelection(selection); } } // Create newly selected embedded streams from the corresponding primary stream. Note that this diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 057f0262d0..396d16968f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -111,7 +111,6 @@ public class DefaultDashChunkSource implements DashChunkSource { private final LoaderErrorThrower manifestLoaderErrorThrower; private final int[] adaptationSetIndices; - private final TrackSelection trackSelection; private final int trackType; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; @@ -120,6 +119,7 @@ public class DefaultDashChunkSource implements DashChunkSource { protected final RepresentationHolder[] representationHolders; + private TrackSelection trackSelection; private DashManifest manifest; private int periodIndex; private IOException fatalError; @@ -222,6 +222,11 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + @Override public void maybeThrowError() throws IOException { if (fatalError != null) { diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 8e9696af70..82e09ab72c 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -39,7 +39,7 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') testImplementation project(modulePrefix + 'testutils-robolectric') 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 261c9b531c..ee5a5f0809 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 @@ -183,17 +183,15 @@ import java.util.Map; } /** - * Selects tracks for use. + * Sets the current track selection. * - * @param trackSelection The track selection. + * @param trackSelection The {@link TrackSelection}. */ - public void selectTracks(TrackSelection trackSelection) { + public void setTrackSelection(TrackSelection trackSelection) { this.trackSelection = trackSelection; } - /** - * Returns the current track selection. - */ + /** Returns the current {@link TrackSelection}. */ public TrackSelection getTrackSelection() { return trackSelection; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 434b6c2011..f7bc913527 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -292,14 +292,17 @@ import java.util.Map; TrackSelection primaryTrackSelection = oldPrimaryTrackSelection; // Select new tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + if (selection == null) { + continue; + } + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + if (trackGroupIndex == primaryTrackGroupIndex) { + primaryTrackSelection = selection; + chunkSource.setTrackSelection(selection); + } + if (streams[i] == null) { enabledTrackGroupCount++; - TrackSelection selection = selections[i]; - int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - if (trackGroupIndex == primaryTrackGroupIndex) { - primaryTrackSelection = selection; - chunkSource.selectTracks(selection); - } streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; if (trackGroupToSampleQueueIndex != null) { diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 095739271e..254a2b2bd1 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -263,7 +263,7 @@ public class HlsMasterPlaylistParserTest { Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); assertThat(closedCaptionFormat.accessibilityChannel).isEqualTo(4); - assertThat(closedCaptionFormat.language).isEqualTo("spa"); + assertThat(closedCaptionFormat.language).isEqualTo("es"); } @Test diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index a2e81fb304..fa67ea1d01 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 59e18195e2..22dfb04f13 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -74,10 +74,10 @@ public class DefaultSsChunkSource implements SsChunkSource { private final LoaderErrorThrower manifestLoaderErrorThrower; private final int streamElementIndex; - private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; private final DataSource dataSource; + private TrackSelection trackSelection; private SsManifest manifest; private int currentManifestChunkOffset; @@ -155,6 +155,11 @@ public class DefaultSsChunkSource implements SsChunkSource { manifest = newManifest; } + @Override + public void updateTrackSelection(TrackSelection trackSelection) { + this.trackSelection = trackSelection; + } + // ChunkSource implementation. @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index b763a484b8..111393140e 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -55,4 +55,11 @@ public interface SsChunkSource extends ChunkSource { * @param newManifest The new manifest. */ void updateManifest(SsManifest newManifest); + + /** + * Updates the track selection. + * + * @param trackSelection The new track selection instance. Must be equivalent to the previous one. + */ + void updateTrackSelection(TrackSelection trackSelection); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 135ee4a58e..e325439d05 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -126,6 +126,7 @@ import java.util.List; stream.release(); streams[i] = null; } else { + stream.getChunkSource().updateTrackSelection(selections[i]); sampleStreamsList.add(stream); } } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 6384bf920f..5182dfccf5 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -41,7 +41,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.media:media:1.0.1' - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index d4a37ea4ef..268219b6d5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.TypedArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.widget.FrameLayout; import java.lang.annotation.Documented; @@ -97,16 +98,16 @@ public final class AspectRatioFrameLayout extends FrameLayout { private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher; - private AspectRatioListener aspectRatioListener; + @Nullable private AspectRatioListener aspectRatioListener; private float videoAspectRatio; - private @ResizeMode int resizeMode; + @ResizeMode private int resizeMode; public AspectRatioFrameLayout(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public AspectRatioFrameLayout(Context context, AttributeSet attrs) { + public AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); resizeMode = RESIZE_MODE_FIT; if (attrs != null) { @@ -136,9 +137,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { /** * Sets the {@link AspectRatioListener}. * - * @param listener The listener to be notified about aspect ratios changes. + * @param listener The listener to be notified about aspect ratios changes, or null to clear a + * listener that was previously set. */ - public void setAspectRatioListener(AspectRatioListener listener) { + public void setAspectRatioListener(@Nullable AspectRatioListener listener) { this.aspectRatioListener = listener; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 383d796692..73bb98a1a0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -281,19 +281,22 @@ public class PlayerControlView extends FrameLayout { private long currentWindowOffset; public PlayerControlView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public PlayerControlView(Context context, AttributeSet attrs) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, attrs); } public PlayerControlView( - Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_player_control_view; rewindMs = DEFAULT_REWIND_MS; 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 cedd3dbec5..260fb9d398 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 @@ -385,6 +385,26 @@ public class PlayerNotificationManager { private boolean wasPlayWhenReady; private int lastPlaybackState; + /** + * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, + * MediaDescriptionAdapter)}. + */ + @Deprecated + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter) { + return createWithNotificationChannel( + context, + channelId, + channelName, + /* channelDescription= */ 0, + notificationId, + mediaDescriptionAdapter); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. @@ -397,8 +417,12 @@ public class PlayerNotificationManager { * * @param context The {@link Context}. * @param channelId The id of the notification channel. - * @param channelName A string resource identifier for the user visible name of the channel. The - * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * @param channelName A string resource identifier for the user visible name of the notification + * channel. The recommended maximum length is 40 characters. The string may be truncated if + * it's too long. + * @param channelDescription A string resource identifier for the user visible description of the + * notification channel, or 0 if no description is provided. The recommended maximum length is + * 300 characters. The value may be truncated if it is too long. * @param notificationId The id of the notification. * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. */ @@ -406,14 +430,37 @@ public class PlayerNotificationManager { Context context, String channelId, @StringRes int channelName, + @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter) { NotificationUtil.createNotificationChannel( - context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); return new PlayerNotificationManager( context, channelId, notificationId, mediaDescriptionAdapter); } + /** + * @deprecated Use {@link #createWithNotificationChannel(Context, String, int, int, int, + * MediaDescriptionAdapter, NotificationListener)}. + */ + @Deprecated + public static PlayerNotificationManager createWithNotificationChannel( + Context context, + String channelId, + @StringRes int channelName, + int notificationId, + MediaDescriptionAdapter mediaDescriptionAdapter, + @Nullable NotificationListener notificationListener) { + return createWithNotificationChannel( + context, + channelId, + channelName, + /* channelDescription= */ 0, + notificationId, + mediaDescriptionAdapter, + notificationListener); + } + /** * Creates a notification manager and a low-priority notification channel with the specified * {@code channelId} and {@code channelName}. The {@link NotificationListener} passed as the last @@ -422,7 +469,9 @@ public class PlayerNotificationManager { * @param context The {@link Context}. * @param channelId The id of the notification channel. * @param channelName A string resource identifier for the user visible name of the channel. The - * recommended maximum length is 40 characters; the value may be truncated if it is too long. + * recommended maximum length is 40 characters. The string may be truncated if it's too long. + * @param channelDescription A string resource identifier for the user visible description of the + * channel, or 0 if no description is provided. * @param notificationId The id of the notification. * @param mediaDescriptionAdapter The {@link MediaDescriptionAdapter}. * @param notificationListener The {@link NotificationListener}. @@ -431,11 +480,12 @@ public class PlayerNotificationManager { Context context, String channelId, @StringRes int channelName, + @StringRes int channelDescription, int notificationId, MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener) { NotificationUtil.createNotificationChannel( - context, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); + context, channelId, channelName, channelDescription, NotificationUtil.IMPORTANCE_LOW); return new PlayerNotificationManager( context, channelId, notificationId, mediaDescriptionAdapter, notificationListener); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index e6bc1a6a71..95d2e1c1bb 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -304,6 +305,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider private boolean controllerHideOnTouch; private int textureViewRotation; private boolean isTouching; + private static final int PICTURE_TYPE_FRONT_COVER = 3; + private static final int PICTURE_TYPE_NOT_SET = -1; public PlayerView(Context context) { this(context, null); @@ -1153,6 +1156,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { + if (!useController || player == null) { + return false; + } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1246,15 +1252,32 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } private boolean setArtworkFromMetadata(Metadata metadata) { + boolean isArtworkSet = false; + int currentPictureType = PICTURE_TYPE_NOT_SET; for (int i = 0; i < metadata.length(); i++) { Metadata.Entry metadataEntry = metadata.get(i); + int pictureType; + byte[] bitmapData; if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + bitmapData = ((ApicFrame) metadataEntry).pictureData; + pictureType = ((ApicFrame) metadataEntry).pictureType; + } else if (metadataEntry instanceof PictureFrame) { + bitmapData = ((PictureFrame) metadataEntry).pictureData; + pictureType = ((PictureFrame) metadataEntry).pictureType; + } else { + continue; + } + // Prefer the first front cover picture. If there aren't any, prefer the first picture. + if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) { Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap)); + currentPictureType = pictureType; + if (currentPictureType == PICTURE_TYPE_FRONT_COVER) { + break; + } } } - return false; + return isArtworkSet; } private boolean setDrawableArtwork(@Nullable Drawable drawable) { @@ -1472,9 +1495,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onSingleTapUp(MotionEvent e) { - if (!useController || player == null) { - return false; - } return toggleControllerVisibility(); } } 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 5d99eda109..0bdc1acc88 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 @@ -53,8 +53,8 @@ public final class SubtitleView extends View implements TextOutput { private final List painters; - private List cues; - private @Cue.TextSizeType int textSizeType; + @Nullable private List cues; + @Cue.TextSizeType private int textSizeType; private float textSize; private boolean applyEmbeddedStyles; private boolean applyEmbeddedFontSizes; @@ -62,10 +62,10 @@ public final class SubtitleView extends View implements TextOutput { private float bottomPaddingFraction; public SubtitleView(Context context) { - this(context, null); + this(context, /* attrs= */ null); } - public SubtitleView(Context context, AttributeSet attrs) { + public SubtitleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); painters = new ArrayList<>(); textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; @@ -246,7 +246,11 @@ public final class SubtitleView extends View implements TextOutput { @Override public void dispatchDraw(Canvas canvas) { - int cueCount = (cues == null) ? 0 : cues.size(); + List cues = this.cues; + if (cues == null || cues.isEmpty()) { + return; + } + int rawViewHeight = getHeight(); // Calculate the cue box bounds relative to the canvas after padding is taken into account. @@ -267,6 +271,7 @@ public final class SubtitleView extends View implements TextOutput { return; } + int cueCount = cues.size(); for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index dd5cfa64a7..5865d3c36d 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -34,7 +34,7 @@ android { dependencies { androidTestImplementation 'androidx.test:rules:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion - androidTestImplementation 'androidx.annotation:annotation:1.0.2' + androidTestImplementation 'androidx.annotation:annotation:1.1.0' androidTestImplementation project(modulePrefix + 'library-core') androidTestImplementation project(modulePrefix + 'library-dash') androidTestImplementation project(modulePrefix + 'library-hls') diff --git a/testutils/build.gradle b/testutils/build.gradle index bdc26d5c19..1ec358b83d 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -41,7 +41,7 @@ dependencies { api 'org.mockito:mockito-core:' + mockitoVersion api 'androidx.test.ext:junit:' + androidXTestVersion api 'androidx.test.ext:truth:' + androidXTestVersion - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' implementation project(modulePrefix + 'library-core') implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 517f1ce2e7..2f91c1926c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -418,7 +418,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc if (actionSchedule != null) { actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } - player.prepare(mediaSource); + player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } catch (Exception e) { handleException(e); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index 3937dabcaf..a933121bc5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -175,17 +175,26 @@ public final class ExtractorAsserts { extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); } + // Seeking to (timeUs=0, position=0) should always work, and cause the same data to be output. + extractorOutput.clearTrackOutputs(); + input.reset(); + consumeTestData(extractor, input, /* timeUs= */ 0, extractorOutput, false); + if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION); + } else { + extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); + } + + // If the SeekMap is seekable, test seeking to 4 positions in the stream. SeekMap seekMap = extractorOutput.seekMap; if (seekMap.isSeekable()) { long durationUs = seekMap.getDurationUs(); for (int j = 0; j < 4; j++) { + extractorOutput.clearTrackOutputs(); long timeUs = (durationUs * j) / 3; long position = seekMap.getSeekPoints(timeUs).first.position; + input.reset(); input.setPosition((int) position); - for (int i = 0; i < extractorOutput.numberOfTracks; i++) { - extractorOutput.trackOutputs.valueAt(i).clear(); - } - consumeTestData(extractor, input, timeUs, extractorOutput, false); extractorOutput.assertOutput(context, file + '.' + j + DUMP_EXTENSION); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index c467bd36af..1a127eeab5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -80,6 +80,15 @@ public final class FakeExtractorInput implements ExtractorInput { failedPeekPositions = new SparseBooleanArray(); } + /** Resets the input to its initial state. */ + public void reset() { + readPosition = 0; + peekPosition = 0; + partiallySatisfiedTargetPositions.clear(); + failedReadPositions.clear(); + failedPeekPositions.clear(); + } + /** * Sets the read and peek positions. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index c6543bd7a5..4022a0ccc1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -70,6 +70,12 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab this.seekMap = seekMap; } + public void clearTrackOutputs() { + for (int i = 0; i < numberOfTracks; i++) { + trackOutputs.valueAt(i).clear(); + } + } + public void assertEquals(FakeExtractorOutput expected) { assertThat(numberOfTracks).isEqualTo(expected.numberOfTracks); assertThat(tracksEnded).isEqualTo(expected.tracksEnded); diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index a3859a9e48..758d22b5d9 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -41,5 +41,5 @@ dependencies { api 'org.robolectric:robolectric:' + robolectricVersion api project(modulePrefix + 'testutils') implementation project(modulePrefix + 'library-core') - implementation 'androidx.annotation:annotation:1.0.2' + implementation 'androidx.annotation:annotation:1.1.0' }