diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d3f7cf8067..bdef903be1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,30 @@ ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling could previously cause downloads to be paused when they should have been able to proceed. +* Fix handling of E-AC-3 streams that contain AC-3 syncframes + ([#6602](https://github.com/google/ExoPlayer/issues/6602)). +* Fix playback of TrueHD streams in Matroska + ([#6845](https://github.com/google/ExoPlayer/issues/6845)). +* Support "twos" codec (big endian PCM) in MP4 + ([#5789](https://github.com/google/ExoPlayer/issues/5789)). +* WAV: Support IMA ADPCM encoded data. +* Show ad group markers in `DefaultTimeBar` even if they are after the end of + the current window + ([#6552](https://github.com/google/ExoPlayer/issues/6552)). +* WAV: + * Support IMA ADPCM encoded data. + * Improve support for G.711 A-law and mu-law encoded data. +* Fix MKV subtitles to disappear when intended instead of lasting until the + next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)). +* Parse \ and \ tags in WebVTT subtitles (rendering is coming + later). +* Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT + subtitles (rendering is coming later). +* OkHttp extension: Upgrade OkHttp dependency to 3.12.7, which fixes a class of + `SocketTimeoutException` issues when using HTTP/2 + ([#4078](https://github.com/google/ExoPlayer/issues/4078)). +* Don't use notification chronometer if playback speed is != 1.0 + ([#6816](https://github.com/google/ExoPlayer/issues/6816)). ### 2.11.1 (2019-12-20) ### diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 17292cec34..0673f7893a 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { Assertions.checkNotNull(format.sampleMimeType); if (!FfmpegLibrary.isAvailable()) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) - || !isOutputSupported(format)) { + } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; 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 5314835d1e..6fa3d888db 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 @@ -64,9 +64,7 @@ import java.util.List; throw new FfmpegDecoderException("Failed to load decoder native libraries."); } Assertions.checkNotNull(format.sampleMimeType); - codecName = - Assertions.checkNotNull( - FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding)); + codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType)); extraData = getExtraData(format.sampleMimeType, format.initializationData); encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; @@ -145,16 +143,12 @@ import java.util.List; nativeContext = 0; } - /** - * Returns the channel count of output audio. May only be called after {@link #decode}. - */ + /** Returns the channel count of output audio. */ public int getChannelCount() { return channelCount; } - /** - * Returns the sample rate of output audio. May only be called after {@link #decode}. - */ + /** Returns the sample rate of output audio. */ public int getSampleRate() { return sampleRate; } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 5b816b8c20..4639851263 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.ext.ffmpeg; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.Log; @@ -65,13 +64,12 @@ public final class FfmpegLibrary { * Returns whether the underlying library supports the specified MIME type. * * @param mimeType The MIME type to check. - * @param encoding The PCM encoding for raw audio. */ - public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) { + public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { return false; } - String codecName = getCodecName(mimeType, encoding); + String codecName = getCodecName(mimeType); if (codecName == null) { return false; } @@ -86,7 +84,7 @@ public final class FfmpegLibrary { * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} * if it's unsupported. */ - /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { + /* package */ static @Nullable String getCodecName(String mimeType) { switch (mimeType) { case MimeTypes.AUDIO_AAC: return "aac"; @@ -116,14 +114,10 @@ public final class FfmpegLibrary { return "flac"; case MimeTypes.AUDIO_ALAC: return "alac"; - case MimeTypes.AUDIO_RAW: - if (encoding == C.ENCODING_PCM_MU_LAW) { - return "pcm_mulaw"; - } else if (encoding == C.ENCODING_PCM_A_LAW) { - return "pcm_alaw"; - } else { - return null; - } + case MimeTypes.AUDIO_MLAW: + return "pcm_mulaw"; + case MimeTypes.AUDIO_ALAW: + return "pcm_alaw"; default: return null; } 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 cad5219883..34b3ad2df5 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 @@ -126,6 +126,8 @@ import java.nio.ByteBuffer; if (targetSampleInLastFrame) { // We are holding the target frame in outputFrameHolder. Set its presentation time now. outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); + // The input position is passed even though it does not indicate the frame containing the + // target sample because the extractor must continue to read from this position. return TimestampSearchResult.targetFoundResult(input.getPosition()); } else if (nextFrameSampleIndex <= targetSampleIndex) { return TimestampSearchResult.underestimatedResult( diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java deleted file mode 100644 index 611197bbe5..0000000000 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java +++ /dev/null @@ -1,76 +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.ext.flac; - -import static com.google.common.truth.Truth.assertThat; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.amr.AmrExtractor; -import com.google.android.exoplayer2.extractor.flv.FlvExtractor; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; -import com.google.android.exoplayer2.extractor.ogg.OggExtractor; -import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; -import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; -import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; -import com.google.android.exoplayer2.extractor.ts.PsExtractor; -import com.google.android.exoplayer2.extractor.ts.TsExtractor; -import com.google.android.exoplayer2.extractor.wav.WavExtractor; -import java.util.ArrayList; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Unit test for {@link DefaultExtractorsFactory}. */ -@RunWith(AndroidJUnit4.class) -public final class DefaultExtractorsFactoryTest { - - @Test - public void testCreateExtractors_returnExpectedClasses() { - DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); - - Extractor[] extractors = defaultExtractorsFactory.createExtractors(); - List> listCreatedExtractorClasses = new ArrayList<>(); - for (Extractor extractor : extractors) { - listCreatedExtractorClasses.add(extractor.getClass()); - } - - Class[] expectedExtractorClassses = - new Class[] { - MatroskaExtractor.class, - FragmentedMp4Extractor.class, - Mp4Extractor.class, - Mp3Extractor.class, - AdtsExtractor.class, - Ac3Extractor.class, - Ac4Extractor.class, - TsExtractor.class, - FlvExtractor.class, - OggExtractor.class, - PsExtractor.class, - WavExtractor.class, - AmrExtractor.class, - FlacExtractor.class - }; - - assertThat(listCreatedExtractorClasses).containsNoDuplicates(); - assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); - } -} diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 3af38397a8..2b4b4854c3 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -39,9 +39,9 @@ dependencies { testImplementation 'org.robolectric:robolectric:' + robolectricVersion // Do not update to 3.13.X or later until minSdkVersion is increased to 21: // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 - // Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop - // us from making this mistake! - api 'com.squareup.okhttp3:okhttp:3.12.5' + // Since OkHttp is distributed as a jar rather than an aar, Gradle won't + // stop us from making this mistake! + api 'com.squareup.okhttp3:okhttp:3.12.7' } ext { diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index fd4e196945..ff59046049 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -5,6 +5,12 @@ public static android.net.Uri buildRawResourceUri(int); } +# Methods accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary { + public static boolean isAvailable(); +} + # Some members of this class are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { *; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index e431b2d899..e926e90d22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -150,10 +150,10 @@ public final class C { /** * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_MP3}, {@link - * #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, - * {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link + * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, + * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -162,11 +162,10 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW, ENCODING_MP3, ENCODING_AC3, ENCODING_E_AC3, @@ -174,15 +173,15 @@ public final class C { ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD, + ENCODING_DOLBY_TRUEHD }) public @interface Encoding {} /** * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -191,11 +190,10 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, - ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW + ENCODING_PCM_FLOAT }) public @interface PcmEncoding {} /** @see AudioFormat#ENCODING_INVALID */ @@ -204,16 +202,14 @@ public final class C { public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; + /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; /** PCM encoding with 24 bits per sample. */ - public static final int ENCODING_PCM_24BIT = 0x80000000; + public static final int ENCODING_PCM_24BIT = 0x20000000; /** PCM encoding with 32 bits per sample. */ - public static final int ENCODING_PCM_32BIT = 0x40000000; + public static final int ENCODING_PCM_32BIT = 0x30000000; /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** Audio encoding for mu-law. */ - public static final int ENCODING_PCM_MU_LAW = 0x10000000; - /** Audio encoding for A-law. */ - public static final int ENCODING_PCM_A_LAW = 0x20000000; /** @see AudioFormat#ENCODING_MP3 */ public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; /** @see AudioFormat#ENCODING_AC3 */ @@ -981,8 +977,8 @@ public final class C { /** * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link - * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or - * {@link #NETWORK_TYPE_OTHER}. + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link + * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -993,6 +989,7 @@ public final class C { NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, + NETWORK_TYPE_5G, NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_ETHERNET, NETWORK_TYPE_OTHER @@ -1010,6 +1007,8 @@ public final class C { public static final int NETWORK_TYPE_3G = 4; /** Network type for a 4G cellular connection. */ public static final int NETWORK_TYPE_4G = 5; + /** Network type for a 5G cellular connection. */ + public static final int NETWORK_TYPE_5G = 9; /** * Network type for cellular connections which cannot be mapped to one of {@link * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. @@ -1017,10 +1016,7 @@ public final class C { public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; /** Network type for an Ethernet connection. */ public static final int NETWORK_TYPE_ETHERNET = 7; - /** - * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN, - * Bluetooth). - */ + /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ public static final int NETWORK_TYPE_OTHER = 8; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 4fb6cec1e8..19ed34405a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -138,13 +138,7 @@ public final class Format implements Parcelable { * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. */ public final int sampleRate; - /** - * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} - * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link - * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link - * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other - * media types. - */ + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ public final @C.PcmEncoding int pcmEncoding; /** * The number of frames to trim from the start of the decoded audio stream, or 0 if not diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 5927b9dd6e..3f3803f5c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -84,6 +84,7 @@ public final class PlaybackStatsListener @Player.State private int playbackState; private boolean isSuppressed; private float playbackSpeed; + private boolean isSeeking; /** * Creates listener for playback stats. @@ -169,6 +170,9 @@ public final class PlaybackStatsListener @Override public void onSessionCreated(EventTime eventTime, String session) { PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + if (isSeeking) { + tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true); + } tracker.onPlayerStateChanged( eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); @@ -288,20 +292,20 @@ public final class PlaybackStatsListener public void onSeekStarted(EventTime eventTime) { sessionManager.updateSessions(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekStarted(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback); } + isSeeking = true; } @Override public void onSeekProcessed(EventTime eventTime) { sessionManager.updateSessions(eventTime); for (String session : playbackStatsTrackers.keySet()) { - if (sessionManager.belongsToSession(eventTime, session)) { - playbackStatsTrackers.get(session).onSeekProcessed(eventTime); - } + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers.get(session).onSeekProcessed(eventTime, belongsToPlayback); } + isSeeking = false; } @Override @@ -563,23 +567,27 @@ public final class PlaybackStatsListener } /** - * Notifies the tracker of the start of a seek in the current playback. + * Notifies the tracker of the start of a seek, including all seeks while the playback is not in + * the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekStarted(EventTime eventTime) { + public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) { isSeeking = true; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** - * Notifies the tracker of a seek has been processed in the current playback. + * Notifies the tracker that a seek has been processed, including all seeks while the playback + * is not in the foreground. * * @param eventTime The {@link EventTime}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. */ - public void onSeekProcessed(EventTime eventTime) { + public void onSeekProcessed(EventTime eventTime, boolean belongsToPlayback) { isSeeking = false; - maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + maybeUpdatePlaybackState(eventTime, belongsToPlayback); } /** @@ -875,7 +883,7 @@ public final class PlaybackStatsListener return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED ? PlaybackStats.PLAYBACK_STATE_ENDED : PlaybackStats.PLAYBACK_STATE_ABANDONED; - } else if (isSeeking) { + } else if (isSeeking && isForeground) { // Seeking takes precedence over errors such that we report a seek while in error state. return PlaybackStats.PLAYBACK_STATE_SEEKING; } else if (hasFatalError) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 05c20939ff..066c9f88ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -31,7 +31,7 @@ import java.nio.ByteBuffer; /** * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the - * definition in ETSI TS 102 366 V1.2.1. + * definition in ETSI TS 102 366 V1.4.1. */ public final class Ac3Util { @@ -39,8 +39,8 @@ public final class Ac3Util { public static final class SyncFrameInfo { /** - * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, - * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. + * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link + * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -114,9 +114,7 @@ public final class Ac3Util { * The number of new samples per (E-)AC-3 audio block. */ private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; - /** - * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1. - */ + /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */ private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; /** * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. @@ -134,20 +132,21 @@ public final class Ac3Util { * Channel counts, indexed by acmod. */ private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; - /** - * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, - 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; - /** - * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) - */ - private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, - 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] BITRATE_BY_HALF_FRMSIZECOD = + new int[] { + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 + }; + /** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */ + private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = + new int[] { + 69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, + 1393 + }; /** - * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS - * 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F. + * The reading position of {@code data} will be modified. * * @param data The AC3SpecificBox to parse. * @param trackId The track identifier to set on the format. @@ -179,8 +178,8 @@ public final class Ac3Util { } /** - * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS - * 102 366 Annex F. The reading position of {@code data} will be modified. + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex + * F. The reading position of {@code data} will be modified. * * @param data The EC3SpecificBox to parse. * @param trackId The track identifier to set on the format. @@ -243,9 +242,10 @@ public final class Ac3Util { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); - boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = data.readBits(5) > 10; data.setPosition(initialPosition); - String mimeType; + @Nullable String mimeType; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; @@ -254,7 +254,7 @@ public final class Ac3Util { boolean lfeon; int channelCount; if (isEac3) { - // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. + // Subsection E.1.2. data.skipBits(16); // syncword switch (data.readBits(2)) { // strmtyp case 0: @@ -472,7 +472,8 @@ public final class Ac3Util { if (data.length < 6) { return C.LENGTH_UNSET; } - boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10; if (isEac3) { int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. frmsiz |= data[3] & 0xFF; // Least significant 8 bits. @@ -485,24 +486,22 @@ public final class Ac3Util { } /** - * Returns the number of audio samples in an AC-3 syncframe. - */ - public static int getAc3SyncframeAudioSampleCount() { - return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; - } - - /** - * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's * position is not modified. * * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ - public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { - // See ETSI TS 102 366 subsection E.1.2.2. - int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; - return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 - : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); + public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10; + if (isEac3) { + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4; + return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + } else { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java index c54e3844a3..b9f1dc5460 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -57,6 +57,11 @@ public final class Ac4Util { /** The channel count of AC-4 stream. */ // TODO: Parse AC-4 stream channel count. private static final int CHANNEL_COUNT_2 = 2; + /** + * The AC-4 sync frame header size for extractor. The seven bytes are 0xAC, 0x40, 0xFF, 0xFF, + * sizeByte1, sizeByte2, sizeByte3. See ETSI TS 103 190-1 V1.3.1, Annex G + */ + public static final int SAMPLE_HEADER_SIZE = 7; /** * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full * header size. @@ -218,7 +223,7 @@ public final class Ac4Util { /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */ public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { // See ETSI TS 103 190-1 V1.3.1, Annex G. - buffer.reset(/* limit= */ 7); + buffer.reset(SAMPLE_HEADER_SIZE); buffer.data[0] = (byte) 0xAC; buffer.data[1] = 0x40; buffer.data[2] = (byte) 0xFF; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 240a8554b7..27abf486fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -1149,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_8BIT: - case C.ENCODING_PCM_A_LAW: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_MU_LAW: case Format.NO_VALUE: default: throw new IllegalArgumentException(); @@ -1166,10 +1164,9 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_DTS_HD: return DtsUtil.parseDtsAudioSampleCount(buffer); case C.ENCODING_AC3: - return Ac3Util.getAc3SyncframeAudioSampleCount(); case C.ENCODING_E_AC3: case C.ENCODING_E_AC3_JOC: - return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); case C.ENCODING_AC4: return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); case C.ENCODING_DOLBY_TRUEHD: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index 7af9d9f074..f57d3b2895 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -81,7 +81,10 @@ public final class DtsUtil { * @return The DTS format parsed from data in the header. */ public static Format parseDtsFormat( - byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { + byte[] frame, + @Nullable String trackId, + @Nullable String language, + @Nullable DrmInitData drmInitData) { ParsableBitArray frameBits = getNormalizedFrameHeader(frame); frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); 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 dfa13134ce..64a2dcfe37 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 @@ -79,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; private static final String TAG = "MediaCodecAudioRenderer"; + /** + * Custom key used to indicate bits per sample by some decoders on Vivo devices. For example + * OMX.vivo.alac.decoder on the Vivo Z1 Pro. + */ + private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample"; private final Context context; private final EventDispatcher eventDispatcher; @@ -566,7 +571,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media mediaFormat.getString(MediaFormat.KEY_MIME)); } else { mediaFormat = outputMediaFormat; - encoding = getPcmEncoding(inputFormat); + if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) { + encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY)); + } else { + encoding = getPcmEncoding(inputFormat); + } } int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 1bfa1897c8..7175b93614 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -29,8 +29,11 @@ import java.nio.ByteBuffer; public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { @C.PcmEncoding int encoding = inputAudioFormat.encoding; - if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT - && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + if (encoding != C.ENCODING_PCM_8BIT + && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN + && encoding != C.ENCODING_PCM_24BIT + && encoding != C.ENCODING_PCM_32BIT) { throw new UnhandledAudioFormatException(inputAudioFormat); } return encoding != C.ENCODING_PCM_16BIT @@ -50,6 +53,9 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + resampledSize = size; + break; case C.ENCODING_PCM_24BIT: resampledSize = (size / 3) * 2; break; @@ -58,8 +64,6 @@ import java.nio.ByteBuffer; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -70,21 +74,28 @@ import java.nio.ByteBuffer; ByteBuffer buffer = replaceOutputBuffer(resampledSize); switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { buffer.put((byte) 0); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + // Big endian to little endian resampling. Swap the byte order. + for (int i = position; i < limit; i += 2) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i)); + } + break; case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. + // 24 -> 16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. + // 32 -> 16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 3)); @@ -92,8 +103,6 @@ import java.nio.ByteBuffer; break; case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 29b772f838..dff81021de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -32,15 +32,17 @@ public final class WavUtil { public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; + public static final int TYPE_PCM = 0x0001; /** WAVE type value for float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; + public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - private static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_ALAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - private static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MLAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; /** * Returns the WAVE format type value for the given {@link C.PcmEncoding}. @@ -57,10 +59,6 @@ public final class WavUtil { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: return TYPE_PCM; - case C.ENCODING_PCM_A_LAW: - return TYPE_A_LAW; - case C.ENCODING_PCM_MU_LAW: - return TYPE_MU_LAW; case C.ENCODING_PCM_FLOAT: return TYPE_FLOAT; case C.ENCODING_INVALID: @@ -81,10 +79,6 @@ public final class WavUtil { return Util.getPcmEncoding(bitsPerSample); case TYPE_FLOAT: return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; - case TYPE_A_LAW: - return C.ENCODING_PCM_A_LAW; - case TYPE_MU_LAW: - return C.ENCODING_PCM_MU_LAW; default: return C.ENCODING_INVALID; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 1f7b6f7098..cdbd37493b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -64,10 +64,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Nullable Constructor flacExtensionExtractorConstructor = null; try { // LINT.IfChange - flacExtensionExtractorConstructor = - Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class) - .getConstructor(); + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = + Boolean.TRUE.equals( + Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary") + .getMethod("isAvailable") + .invoke(/* obj= */ null)); + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the FLAC extension. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java index 1e498cb677..f014eaa565 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -167,7 +167,7 @@ public final class FlacFrameReader { * @param data The array to read the data from, whose position must correspond to the block size * bits. * @param blockSizeKey The key in the block size lookup table. - * @return The block size in samples. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. */ public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { switch (blockSizeKey) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java index 8a64d4243c..79dd20065b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -256,15 +256,18 @@ public final class FlacExtractor implements Extractor { // Copy more bytes into the buffer. int currentLimit = buffer.limit(); - int bytesRead = - input.read( - buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); - boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; - if (!foundEndOfInput) { - buffer.setLimit(currentLimit + bytesRead); - } else if (buffer.bytesLeft() == 0) { - outputSampleMetadata(); - return Extractor.RESULT_END_OF_INPUT; + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } } // Search for a frame. @@ -272,7 +275,7 @@ public final class FlacExtractor implements Extractor { // Skip frame search on the bytes within the minimum frame size. if (currentFrameBytesWritten < minFrameSize) { - buffer.skipBytes(Math.min(minFrameSize, buffer.bytesLeft())); + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); } long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index b10f2bf80b..4a904844ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -69,9 +69,20 @@ import java.util.Collections; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_MLAW; - int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, - Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ type, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 1, + /* sampleRate= */ 8000, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); output.format(format); hasOutputFormat = true; } else if (audioFormat != AUDIO_FORMAT_AAC) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ed2acc5898..8812d2857e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -1250,10 +1250,10 @@ public class MatroskaExtractor implements Extractor { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { if (blockSampleCount > 1) { Log.w(TAG, "Skipping subtitle sample in laced block."); - } else if (durationUs == C.TIME_UNSET) { + } else if (blockDurationUs == C.TIME_UNSET) { Log.w(TAG, "Skipping subtitle sample with no duration."); } else { - setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); // Note: If we ever want to support DRM protected subtitles then we'll need to output the // appropriate encryption data here. track.output.sampleData(subtitleSample, subtitleSample.limit()); @@ -1829,10 +1829,8 @@ public class MatroskaExtractor implements Extractor { chunkSize += size; chunkOffset = offset; // The offset is to the end of the sample. if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { - // We haven't read enough samples to output a chunk. - return; + outputPendingSampleMetadata(track); } - outputPendingSampleMetadata(track); } public void outputPendingSampleMetadata(Track track) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 572efed1af..e86a873ed5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -379,6 +379,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_dfLa = 0x64664c61; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; + public final int type; public Atom(int type) { 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 f6b4f4d463..8f2a244d59 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 @@ -798,6 +798,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -1086,6 +1087,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int channelCount; int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1147,6 +1149,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { @@ -1233,9 +1239,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } if (out.format == null && mimeType != null) { - // TODO: Determine the correct PCM encoding. - @C.PcmEncoding int pcmEncoding = - MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, initializationData == null ? null : Collections.singletonList(initializationData), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 792545b610..a4a70ce7e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -168,7 +168,6 @@ public class FragmentedMp4Extractor implements Extractor { private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; private boolean processSeiNalUnitPayload; - private boolean isAc4HeaderRequired; // Extractor output. @MonotonicNonNull private ExtractorOutput extractorOutput; @@ -302,7 +301,6 @@ public class FragmentedMp4Extractor implements Extractor { pendingMetadataSampleBytes = 0; pendingSeekTimeUs = timeUs; containerAtoms.clear(); - isAc4HeaderRequired = false; enterReadingAtomHeaderState(); } @@ -1222,7 +1220,6 @@ public class FragmentedMp4Extractor implements Extractor { * @throws InterruptedException If the thread is interrupted. */ private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { - int outputSampleEncryptionDataSize = 0; if (parserState == STATE_READING_SAMPLE_START) { if (currentTrackBundle == null) { @Nullable TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles); @@ -1270,11 +1267,14 @@ public class FragmentedMp4Extractor implements Extractor { } sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); sampleSize += sampleBytesWritten; - outputSampleEncryptionDataSize = sampleBytesWritten; + if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; + } parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); } TrackFragment fragment = currentTrackBundle.fragment; @@ -1339,14 +1339,6 @@ public class FragmentedMp4Extractor implements Extractor { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize - outputSampleEncryptionDataSize, scratch); - int length = scratch.limit(); - output.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 971cc27d13..ff5dcb931d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -110,9 +110,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { @Nullable private ParsableByteArray atomData; private int sampleTrackIndex; + private int sampleBytesRead; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; - private boolean isAc4HeaderRequired; // Extractor outputs. @MonotonicNonNull private ExtractorOutput extractorOutput; @@ -160,9 +160,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms.clear(); atomHeaderBytesRead = 0; sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = false; if (position == 0) { enterReadingAtomHeaderState(); } else if (tracks != null) { @@ -507,15 +507,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleTrackIndex == C.INDEX_UNSET) { return RESULT_END_OF_INPUT; } - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType); } Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex]; - long skipAmount = position - inputPosition + sampleBytesWritten; + long skipAmount = position - inputPosition + sampleBytesRead; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; @@ -543,6 +541,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); int nalLengthInt = nalLength.readInt(); if (nalLengthInt < 0) { @@ -557,21 +556,23 @@ public final class Mp4Extractor implements Extractor, SeekMap { } else { // Write the payload of the NAL unit. int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - trackOutput.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + if (sampleBytesWritten == 0) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } @@ -580,6 +581,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleIndex++; sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; return RESULT_CONTINUE; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index cd07a40c6d..af5efc35a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; 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.audio.Ac3Util; @@ -23,11 +24,15 @@ import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. @@ -47,10 +52,10 @@ public final class Ac3Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; @State private int state; private int bytesRead; @@ -60,7 +65,7 @@ public final class Ac3Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -78,7 +83,7 @@ public final class Ac3Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac3Reader(String language) { + public Ac3Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +111,7 @@ public final class Ac3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,19 +191,28 @@ public final class Ac3Reader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ - @SuppressWarnings("ReferenceEquality") + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); - if (format == null || frameInfo.channelCount != format.channelCount + if (format == null + || frameInfo.channelCount != format.channelCount || frameInfo.sampleRate != format.sampleRate - || frameInfo.mimeType != format.sampleMimeType) { - format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, - Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, - null, 0, language); + || Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) { + format = + Format.createAudioSampleFormat( + formatId, + frameInfo.mimeType, + null, + Format.NO_VALUE, + Format.NO_VALUE, + frameInfo.channelCount, + frameInfo.sampleRate, + null, + null, + 0, + language); output.format(format); } sampleSize = frameInfo.frameSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java index 48bd07fce4..096eb81119 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; 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.audio.Ac4Util; @@ -23,12 +24,15 @@ import com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Parses a continuous AC-4 byte stream and extracts individual samples. */ public final class Ac4Reader implements ElementaryStreamReader { @@ -44,10 +48,10 @@ public final class Ac4Reader implements ElementaryStreamReader { private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String trackFormatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; @State private int state; private int bytesRead; @@ -58,7 +62,7 @@ public final class Ac4Reader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -74,7 +78,7 @@ public final class Ac4Reader implements ElementaryStreamReader { * * @param language Track language. */ - public Ac4Reader(String language) { + public Ac4Reader(@Nullable String language) { headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -95,7 +99,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); - trackFormatId = generator.getFormatId(); + formatId = generator.getFormatId(); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @@ -106,6 +110,7 @@ public final class Ac4Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -185,7 +190,7 @@ public final class Ac4Reader implements ElementaryStreamReader { } /** Parses the sample header. */ - @SuppressWarnings("ReferenceEquality") + @RequiresNonNull("output") private void parseHeader() { headerScratchBits.setPosition(0); SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); @@ -195,7 +200,7 @@ public final class Ac4Reader implements ElementaryStreamReader { || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { format = Format.createAudioSampleFormat( - trackFormatId, + formatId, MimeTypes.AUDIO_AC4, /* codecs= */ null, /* bitrate= */ Format.NO_VALUE, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 589b543170..56ffc4500e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -23,13 +24,18 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous ADTS byte stream and extracts individual frames. @@ -62,11 +68,11 @@ public final class AdtsReader implements ElementaryStreamReader { private final boolean exposeId3; private final ParsableBitArray adtsScratch; private final ParsableByteArray id3HeaderBuffer; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; - private TrackOutput id3Output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private TrackOutput id3Output; private int state; private int bytesRead; @@ -90,7 +96,7 @@ public final class AdtsReader implements ElementaryStreamReader { // Used when reading the samples. private long timeUs; - private TrackOutput currentOutput; + @MonotonicNonNull private TrackOutput currentOutput; private long currentSampleDuration; /** @@ -104,7 +110,7 @@ public final class AdtsReader implements ElementaryStreamReader { * @param exposeId3 True if the reader should expose ID3 information. * @param language Track language. */ - public AdtsReader(boolean exposeId3, String language) { + public AdtsReader(boolean exposeId3, @Nullable String language) { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); @@ -130,6 +136,7 @@ public final class AdtsReader implements ElementaryStreamReader { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + currentOutput = output; if (exposeId3) { idGenerator.generateNewId(); id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); @@ -147,6 +154,7 @@ public final class AdtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + assertTracksCreated(); while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -425,9 +433,8 @@ public final class AdtsReader implements ElementaryStreamReader { return true; } - /** - * Parses the Id3 header. - */ + /** Parses the Id3 header. */ + @RequiresNonNull("id3Output") private void parseId3Header() { id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); @@ -435,9 +442,8 @@ public final class AdtsReader implements ElementaryStreamReader { id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); @@ -487,9 +493,8 @@ public final class AdtsReader implements ElementaryStreamReader { setReadingSampleState(output, sampleDurationUs, 0, sampleSize); } - /** - * Reads the rest of the sample - */ + /** Reads the rest of the sample */ + @RequiresNonNull("currentOutput") private void readSample(ParsableByteArray data) { int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); currentOutput.sampleData(data, bytesToRead); @@ -501,4 +506,10 @@ public final class AdtsReader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "currentOutput", "id3Output"}) + private void assertTracksCreated() { + Assertions.checkNotNull(output); + Util.castNonNull(currentOutput); + Util.castNonNull(id3Output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 24d17f4956..480edb0a19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.text.cea.Cea708InitializationData; @@ -134,6 +135,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact return new SparseArray<>(); } + @Nullable @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { switch (streamType) { @@ -247,7 +249,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact // Skip reserved (8). scratchDescriptorData.skipBytes(1); - List initializationData = null; + @Nullable List initializationData = null; // The wide_aspect_ratio flag only has meaning for CEA-708. if (isDigital) { boolean isWideAspectRatio = (flags & 0x40) != 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 1f9b0e79d4..127405d661 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.DtsUtil; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous DTS byte stream and extracts individual samples. @@ -35,10 +39,10 @@ public final class DtsReader implements ElementaryStreamReader { private static final int HEADER_SIZE = 18; private final ParsableByteArray headerScratchBytes; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; private int state; private int bytesRead; @@ -48,7 +52,7 @@ public final class DtsReader implements ElementaryStreamReader { // Used when parsing the header. private long sampleDurationUs; - private Format format; + @MonotonicNonNull private Format format; private int sampleSize; // Used when reading the samples. @@ -59,7 +63,7 @@ public final class DtsReader implements ElementaryStreamReader { * * @param language Track language. */ - public DtsReader(String language) { + public DtsReader(@Nullable String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); state = STATE_FINDING_SYNC; this.language = language; @@ -86,6 +90,7 @@ public final class DtsReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SYNC: @@ -162,9 +167,8 @@ public final class DtsReader implements ElementaryStreamReader { return false; } - /** - * Parses the sample header. - */ + /** Parses the sample header. */ + @RequiresNonNull("output") private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index 3f0a772b1c..146f663bfd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -64,12 +64,12 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { Format.createImageSampleFormat( idGenerator.getFormatId(), MimeTypes.APPLICATION_DVBSUBS, - null, + /* codecs= */ null, Format.NO_VALUE, - 0, + /* selectionFlags= */ 0, Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, - null)); + /* drmInitData= */ null)); outputs[i] = output; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index e7f2c1935b..4d2018ef86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses a continuous H262 byte stream and extracts individual frames. @@ -38,27 +42,27 @@ public final class H262Reader implements ElementaryStreamReader { private static final int START_GROUP = 0xB8; private static final int START_USER_DATA = 0xB2; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. private static final double[] FRAME_RATE_VALUES = new double[] { 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; + @Nullable private final UserDataReader userDataReader; + @Nullable private final ParsableByteArray userDataParsable; + + // State that should be reset on seek. + @Nullable private final NalUnitTargetBuffer userData; + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + private long totalBytesWritten; + private boolean startedFirstSample; + // State that should not be reset on seek. private boolean hasOutputFormat; private long frameDurationUs; - private final UserDataReader userDataReader; - private final ParsableByteArray userDataParsable; - - // State that should be reset on seek. - private final boolean[] prefixFlags; - private final CsdBuffer csdBuffer; - private final NalUnitTargetBuffer userData; - private long totalBytesWritten; - private boolean startedFirstSample; - // Per packet state that gets reset at the start of each packet. private long pesTimeUs; @@ -72,7 +76,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - /* package */ H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(@Nullable UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); @@ -89,7 +93,7 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - if (userDataReader != null) { + if (userData != null) { userData.reset(); } totalBytesWritten = 0; @@ -114,6 +118,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -130,7 +135,7 @@ public final class H262Reader implements ElementaryStreamReader { if (!hasOutputFormat) { csdBuffer.onData(dataArray, offset, limit); } - if (userDataReader != null) { + if (userData != null) { userData.appendToNalUnit(dataArray, offset, limit); } return; @@ -157,7 +162,7 @@ public final class H262Reader implements ElementaryStreamReader { hasOutputFormat = true; } } - if (userDataReader != null) { + if (userData != null) { int bytesAlreadyPassed = 0; if (lengthToStartCode > 0) { userData.appendToNalUnit(dataArray, offset, startCodeOffset); @@ -167,8 +172,8 @@ public final class H262Reader implements ElementaryStreamReader { if (userData.endNalUnit(bytesAlreadyPassed)) { int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); - userDataParsable.reset(userData.nalData, unescapedLength); - userDataReader.consume(sampleTimeUs, userDataParsable); + Util.castNonNull(userDataParsable).reset(userData.nalData, unescapedLength); + Util.castNonNull(userDataReader).consume(sampleTimeUs, userDataParsable); } if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { @@ -211,10 +216,10 @@ public final class H262Reader implements ElementaryStreamReader { * * @param csdBuffer The csd buffer. * @param formatId The id for the generated format. May be null. - * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or - * 0 if the duration could not be determined. + * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if + * the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index d249c1b9da..011b3fd7b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -23,15 +23,21 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil.SpsData; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H264 byte stream and extracts individual frames. @@ -51,9 +57,9 @@ public final class H264Reader implements ElementaryStreamReader { private long totalBytesWritten; private final boolean[] prefixFlags; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -87,13 +93,15 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; + randomAccessIndicator = false; NalUnitUtil.clearPrefixFlags(prefixFlags); sps.reset(); pps.reset(); sei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; - randomAccessIndicator = false; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -113,6 +121,8 @@ public final class H264Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + int offset = data.getPosition(); int limit = data.limit(); byte[] dataArray = data.data; @@ -159,6 +169,7 @@ public final class H264Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.startNalUnit(nalUnitType); @@ -168,6 +179,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.appendToNalUnit(dataArray, offset, limit); @@ -177,6 +189,7 @@ public final class H264Reader implements ElementaryStreamReader { sampleReader.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (!hasOutputFormat || sampleReader.needsSpsPps()) { sps.endNalUnit(discardPadding); @@ -237,6 +250,12 @@ public final class H264Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + /** Consumes a stream of NAL units and outputs samples. */ private static final class SampleReader { @@ -478,7 +497,7 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isComplete; private boolean hasSliceType; - private SpsData spsData; + @Nullable private SpsData spsData; private int nalRefIdc; private int sliceType; private int frameNum; @@ -542,6 +561,8 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { // See ISO 14496-10 subsection 7.4.1.2.4. + SpsData spsData = Assertions.checkStateNotNull(this.spsData); + SpsData otherSpsData = Assertions.checkStateNotNull(other.spsData); return isComplete && (!other.isComplete || frameNum != other.frameNum @@ -552,15 +573,15 @@ public final class H264Reader implements ElementaryStreamReader { && bottomFieldFlag != other.bottomFieldFlag) || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) || (spsData.picOrderCountType == 0 - && other.spsData.picOrderCountType == 0 + && otherSpsData.picOrderCountType == 0 && (picOrderCntLsb != other.picOrderCntLsb || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) || (spsData.picOrderCountType == 1 - && other.spsData.picOrderCountType == 1 + && otherSpsData.picOrderCountType == 1 && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) || idrPicFlag != other.idrPicFlag - || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + || (idrPicFlag && idrPicId != other.idrPicId)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 88bde53746..c86cf51866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -20,12 +20,18 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; +import com.google.android.exoplayer2.util.Util; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous H.265 byte stream and extracts individual frames. @@ -46,9 +52,9 @@ public final class H265Reader implements ElementaryStreamReader { private final SeiReader seiReader; - private String formatId; - private TrackOutput output; - private SampleReader sampleReader; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private SampleReader sampleReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -84,14 +90,16 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void seek() { + totalBytesWritten = 0; NalUnitUtil.clearPrefixFlags(prefixFlags); vps.reset(); sps.reset(); pps.reset(); prefixSei.reset(); suffixSei.reset(); - sampleReader.reset(); - totalBytesWritten = 0; + if (sampleReader != null) { + sampleReader.reset(); + } } @Override @@ -111,6 +119,8 @@ public final class H265Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + assertTracksCreated(); + while (data.bytesLeft() > 0) { int offset = data.getPosition(); int limit = data.limit(); @@ -160,6 +170,7 @@ public final class H265Reader implements ElementaryStreamReader { // Do nothing. } + @RequiresNonNull("sampleReader") private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { if (hasOutputFormat) { sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); @@ -172,6 +183,7 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.startNalUnit(nalUnitType); } + @RequiresNonNull("sampleReader") private void nalUnitData(byte[] dataArray, int offset, int limit) { if (hasOutputFormat) { sampleReader.readNalUnitData(dataArray, offset, limit); @@ -184,6 +196,7 @@ public final class H265Reader implements ElementaryStreamReader { suffixSei.appendToNalUnit(dataArray, offset, limit); } + @RequiresNonNull({"output", "sampleReader"}) private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { if (hasOutputFormat) { sampleReader.endNalUnit(position, offset); @@ -214,8 +227,11 @@ public final class H265Reader implements ElementaryStreamReader { } } - private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, - NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { + private static Format parseMediaFormat( + @Nullable String formatId, + NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, + NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -389,6 +405,12 @@ public final class H265Reader implements ElementaryStreamReader { } } + @EnsuresNonNull({"output", "sampleReader"}) + private void assertTracksCreated() { + Assertions.checkStateNotNull(output); + Util.castNonNull(sampleReader); + } + private static final class SampleReader { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 77ec48d0a7..615d2f8c2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses ID3 data and extracts individual text information frames. @@ -36,7 +38,7 @@ public final class Id3Reader implements ElementaryStreamReader { private final ParsableByteArray id3Header; - private TrackOutput output; + @MonotonicNonNull private TrackOutput output; // State that should be reset on seek. private boolean writingSample; @@ -76,6 +78,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample) { return; } @@ -106,6 +109,7 @@ public final class Id3Reader implements ElementaryStreamReader { @Override public void packetFinished() { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 4ad9adfa2a..1c8131feaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -23,11 +23,14 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses and extracts samples from an AAC/LATM elementary stream. @@ -43,14 +46,14 @@ public final class LatmReader implements ElementaryStreamReader { private static final int SYNC_BYTE_FIRST = 0x56; private static final int SYNC_BYTE_SECOND = 0xE0; - private final String language; + @Nullable private final String language; private final ParsableByteArray sampleDataBuffer; private final ParsableBitArray sampleBitArray; // Track output info. - private TrackOutput output; - private Format format; - private String formatId; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private String formatId; + @MonotonicNonNull private Format format; // Parser state info. private int state; @@ -99,6 +102,7 @@ public final class LatmReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) throws ParserException { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. int bytesToRead; while (data.bytesLeft() > 0) { switch (state) { @@ -150,6 +154,7 @@ public final class LatmReader implements ElementaryStreamReader { * * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. */ + @RequiresNonNull("output") private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { boolean useSameStreamMux = data.readBit(); if (!useSameStreamMux) { @@ -173,9 +178,8 @@ public final class LatmReader implements ElementaryStreamReader { } } - /** - * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. - */ + /** Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ + @RequiresNonNull("output") private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { int audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; @@ -198,9 +202,19 @@ public final class LatmReader implements ElementaryStreamReader { data.setPosition(startPosition); byte[] initData = new byte[(readBits + 7) / 8]; data.readBits(initData, 0, readBits); - Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, - Collections.singletonList(initData), null, 0, language); + Format format = + Format.createAudioSampleFormat( + formatId, + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRateHz, + Collections.singletonList(initData), + /* drmInitData= */ null, + /* selectionFlags= */ 0, + language); if (!format.equals(this.format)) { this.format = format; sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; @@ -280,6 +294,7 @@ public final class LatmReader implements ElementaryStreamReader { } } + @RequiresNonNull("output") private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { // The start of sample data in int bitPosition = data.getPosition(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index 393e297818..5f41a23246 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -21,7 +21,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses a continuous MPEG Audio byte stream and extracts individual frames. @@ -36,10 +40,10 @@ public final class MpegAudioReader implements ElementaryStreamReader { private final ParsableByteArray headerScratch; private final MpegAudioHeader header; - private final String language; + @Nullable private final String language; - private String formatId; - private TrackOutput output; + @MonotonicNonNull private TrackOutput output; + @MonotonicNonNull private String formatId; private int state; private int frameBytesRead; @@ -59,7 +63,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { this(null); } - public MpegAudioReader(String language) { + public MpegAudioReader(@Nullable String language) { state = STATE_FINDING_HEADER; // The first byte of an MPEG Audio frame header is always 0xFF. headerScratch = new ParsableByteArray(4); @@ -89,6 +93,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { @Override public void consume(ParsableByteArray data) { + Assertions.checkStateNotNull(output); // Asserts that createTracks has been called. while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_HEADER: @@ -146,20 +151,21 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remaining two bytes of the frame header. - *

- * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, + * + *

If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, * the media format is output if this has not previously occurred, the four header bytes are * output as sample data, and the position of the source is advanced to the byte that immediately * follows the header. - *

- * If a frame header is read in full but cannot be parsed then the state is changed to - * {@link #STATE_READING_HEADER}. - *

- * If a frame header is not read in full then the position of the source is advanced to the limit, - * and the method should be called again with the next source to continue the read. + * + *

If a frame header is read in full but cannot be parsed then the state is changed to {@link + * #STATE_READING_HEADER}. + * + *

If a frame header is not read in full then the position of the source is advanced to the + * limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readHeaderRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); @@ -195,16 +201,17 @@ public final class MpegAudioReader implements ElementaryStreamReader { /** * Attempts to read the remainder of the frame. - *

- * If a frame is read in full then true is returned. The frame will have been output, and the + * + *

If a frame is read in full then true is returned. The frame will have been output, and the * position of the source will have been advanced to the byte that immediately follows the end of * the frame. - *

- * If a frame is not read in full then the position of the source will have been advanced to the - * limit, and the method should be called again with the next source to continue the read. + * + *

If a frame is not read in full then the position of the source will have been advanced to + * the limit, and the method should be called again with the next source to continue the read. * * @param source The source from which to read. */ + @RequiresNonNull("output") private void readFrameRemainder(ParsableByteArray source) { int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); output.sampleData(source, bytesToRead); @@ -219,5 +226,4 @@ public final class MpegAudioReader implements ElementaryStreamReader { frameBytesRead = 0; state = STATE_FINDING_HEADER; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index ff755f4ece..d5d32a6d96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Parses PES packet data and extracts samples. @@ -45,7 +49,7 @@ public final class PesReader implements TsPayloadReader { private int state; private int bytesRead; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; private boolean ptsFlag; private boolean dtsFlag; private boolean seenFirstDts; @@ -79,6 +83,8 @@ public final class PesReader implements TsPayloadReader { @Override public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called. + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { switch (state) { case STATE_FINDING_HEADER: @@ -119,7 +125,7 @@ public final class PesReader implements TsPayloadReader { int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); // Read as much of the extended header as we're interested in, and skip the rest. if (continueRead(data, pesScratch.data, readLength) - && continueRead(data, null, extendedHeaderLength)) { + && continueRead(data, /* target= */ null, extendedHeaderLength)) { parseHeaderExtension(); flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; reader.packetStarted(timeUs, flags); @@ -162,7 +168,8 @@ public final class PesReader implements TsPayloadReader { * @param targetLength The target length of the read. * @return Whether the target length has been reached. */ - private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + private boolean continueRead( + ParsableByteArray source, @Nullable byte[] target, int targetLength) { int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); if (bytesToRead <= 0) { return true; @@ -207,6 +214,7 @@ public final class PesReader implements TsPayloadReader { return true; } + @RequiresNonNull("timestampAdjuster") private void parseHeaderExtension() { pesScratch.setPosition(0); timeUs = C.TIME_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index fec108fd5f..3f10a454fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -25,10 +26,13 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Extracts data from the MPEG-2 PS container format. @@ -67,8 +71,8 @@ public final class PsExtractor implements Extractor { private long lastTrackPosition; // Accessed only by the loading thread. - private PsBinarySearchSeeker psBinarySearchSeeker; - private ExtractorOutput output; + @Nullable private PsBinarySearchSeeker psBinarySearchSeeker; + @MonotonicNonNull private ExtractorOutput output; private boolean hasOutputSeekMap; public PsExtractor() { @@ -160,6 +164,7 @@ public final class PsExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + Assertions.checkStateNotNull(output); // Asserts init has been called. long inputLength = input.getLength(); boolean canReadDuration = inputLength != C.LENGTH_UNSET; @@ -221,7 +226,7 @@ public final class PsExtractor implements Extractor { PesReader payloadReader = psPayloadReaders.get(streamId); if (!foundAllTracks) { if (payloadReader == null) { - ElementaryStreamReader elementaryStreamReader = null; + @Nullable ElementaryStreamReader elementaryStreamReader = null; if (streamId == PRIVATE_STREAM_1) { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only @@ -278,6 +283,7 @@ public final class PsExtractor implements Extractor { // Internals. + @RequiresNonNull("output") private void maybeOutputSeekMap(long inputLength) { if (!hasOutputSeekMap) { hasOutputSeekMap = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index d032ef5883..2541db07a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -45,7 +46,7 @@ public final class SeiReader { idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), "Invalid closed caption mime type provided: " + channelMimeType); @@ -69,5 +70,4 @@ public final class SeiReader { public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { CeaUtil.consume(pesTimeUs, seiBuffer, outputs); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 27838d4c25..6747a04916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -19,17 +19,21 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses splice info sections as defined by SCTE35. */ public final class SpliceInfoSectionReader implements SectionPayloadReader { - private TimestampAdjuster timestampAdjuster; - private TrackOutput output; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TrackOutput output; private boolean formatDeclared; @Override @@ -44,6 +48,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { @Override public void consume(ParsableByteArray sectionData) { + assertInitialized(); if (!formatDeclared) { if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { // There is not enough information to initialize the timestamp adjuster. @@ -59,4 +64,9 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { sampleSize, 0, null); } + @EnsuresNonNull({"timestampAdjuster", "output"}) + private void assertInitialized() { + Assertions.checkStateNotNull(timestampAdjuster); + Util.castNonNull(output); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2cd7398d7c..35e8806a6f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -21,6 +21,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -587,8 +588,11 @@ public final class TsExtractor implements Extractor { continue; } - TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader - : payloadReaderFactory.createPayloadReader(streamType, esInfo); + @Nullable + TsPayloadReader reader = + mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 + ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); if (mode != MODE_HLS || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { trackIdToPidScratch.put(trackId, elementaryPid); @@ -602,7 +606,7 @@ public final class TsExtractor implements Extractor { int trackPid = trackIdToPidScratch.valueAt(i); trackIds.put(trackId, true); trackPids.put(trackPid, true); - TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + @Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); if (reader != null) { if (reader != id3Reader) { reader.init(timestampAdjuster, output, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index af27235257..03ed10ff0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -53,11 +54,11 @@ public interface TsPayloadReader { * * @param streamType Stream type value as defined in the PMT entry or associated descriptors. * @param esInfo Information associated to the elementary stream provided in the PMT. - * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid. + * @return A {@link TsPayloadReader} for the packet stream carried by the provided pid, or * {@code null} if the stream is not supported. */ + @Nullable TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); - } /** @@ -66,18 +67,21 @@ public interface TsPayloadReader { final class EsInfo { public final int streamType; - public final String language; + @Nullable public final String language; public final List dvbSubtitleInfos; public final byte[] descriptorBytes; /** - * @param streamType The type of the stream as defined by the - * {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. + * @param streamType The type of the stream as defined by the {@link TsExtractor}{@code + * .TS_STREAM_TYPE_*}. * @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @param dvbSubtitleInfos Information about DVB subtitles associated to the stream. * @param descriptorBytes The descriptor bytes associated to the stream. */ - public EsInfo(int streamType, String language, List dvbSubtitleInfos, + public EsInfo( + int streamType, + @Nullable String language, + @Nullable List dvbSubtitleInfos, byte[] descriptorBytes) { this.streamType = streamType; this.language = language; @@ -134,6 +138,7 @@ public interface TsPayloadReader { this.firstTrackId = firstTrackId; this.trackIdIncrement = trackIdIncrement; trackId = ID_UNSET; + formatId = ""; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java index 724eba1d9a..739e5341b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/UserDataReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -44,7 +45,7 @@ import java.util.List; idGenerator.generateNewId(); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); Format channelFormat = closedCaptionFormats.get(i); - String channelMimeType = channelFormat.sampleMimeType; + @Nullable String channelMimeType = channelFormat.sampleMimeType; Assertions.checkArgument( MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java new file mode 100644 index 0000000000..4d93bd5ac5 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.extractor.ts; + +import com.google.android.exoplayer2.util.NonNullApi; 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 37edb07a1a..45a8c24e67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -28,8 +28,11 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from WAV byte streams. @@ -46,9 +49,9 @@ public final class WavExtractor implements Extractor { /** Factory for {@link WavExtractor} instances. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - private OutputWriter outputWriter; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private OutputWriter outputWriter; private int dataStartPosition; private long dataEndPosition; @@ -84,6 +87,7 @@ public final class WavExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + assertInitialized(); if (outputWriter == null) { WavHeader header = WavHeaderReader.peek(input); if (header == null) { @@ -91,12 +95,34 @@ public final class WavExtractor implements Extractor { throw new ParserException("Unsupported or unrecognized wav header."); } - @C.PcmEncoding - int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported WAV format type: " + header.formatType); + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); } - outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding); } if (dataStartPosition == C.POSITION_UNSET) { @@ -113,6 +139,12 @@ public final class WavExtractor implements Extractor { return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + /** Writes to the extractor's output. */ private interface OutputWriter { @@ -150,61 +182,56 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException; } - private static final class PcmOutputWriter implements OutputWriter { + private static final class PassthroughOutputWriter implements OutputWriter { private final ExtractorOutput extractorOutput; private final TrackOutput trackOutput; private final WavHeader header; - private final @C.PcmEncoding int pcmEncoding; - private final int targetSampleSize; + private final Format format; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + /** The time at which the writer was last {@link #reset}. */ private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ private long outputFrameCount; - private int pendingBytes; - public PcmOutputWriter( + public PassthroughOutputWriter( ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header, - @C.PcmEncoding int pcmEncoding) { + String mimeType, + @C.PcmEncoding int pcmEncoding) + throws ParserException { this.extractorOutput = extractorOutput; this.trackOutput = trackOutput; this.header = header; - this.pcmEncoding = pcmEncoding; - // For PCM blocks correspond to single frames. This is validated in init(int, long). - int bytesPerFrame = header.blockSize; - targetSampleSize = - Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); - } - @Override - public void reset(long timeUs) { - startTimeUs = timeUs; - outputFrameCount = 0; - pendingBytes = 0; - } - - @Override - public void init(int dataStartPosition, long dataEndPosition) throws ParserException { - // Validate the header. int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. if (header.blockSize != bytesPerFrame) { throw new ParserException( "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); } - // Output the seek map. - extractorOutput.seekMap( - new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); - - // Output the format. - Format format = + targetSampleSizeBytes = + Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = Format.createAudioSampleFormat( /* id= */ null, - MimeTypes.AUDIO_RAW, + mimeType, /* codecs= */ null, - /* bitrate= */ header.averageBytesPerSecond * 8, - targetSampleSize, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, + /* maxInputSize= */ targetSampleSizeBytes, header.numChannels, header.frameRateHz, pcmEncoding, @@ -212,6 +239,19 @@ public final class WavExtractor implements Extractor { /* drmInitData= */ null, /* selectionFlags= */ 0, /* language= */ null); + } + + @Override + public void reset(long timeUs) { + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); trackOutput.format(format); } @@ -220,34 +260,303 @@ public final class WavExtractor implements Extractor { throws IOException, InterruptedException { // Write sample data until we've reached the target sample size, or the end of the data. boolean endOfSampleData = bytesLeft == 0; - while (!endOfSampleData && pendingBytes < targetSampleSize) { - int bytesToRead = (int) Math.min(targetSampleSize - pendingBytes, bytesLeft); + while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); if (bytesAppended == RESULT_END_OF_INPUT) { endOfSampleData = true; } else { - pendingBytes += bytesAppended; + pendingOutputBytes += bytesAppended; } } // Write the corresponding sample metadata. Samples must be a whole number of frames. It's - // possible pendingBytes is not a whole number of frames if the stream ended unexpectedly. + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. int bytesPerFrame = header.blockSize; - int pendingFrames = pendingBytes / bytesPerFrame; + int pendingFrames = pendingOutputBytes / bytesPerFrame; if (pendingFrames > 0) { long timeUs = startTimeUs + Util.scaleLargeTimestamp( outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); int size = pendingFrames * bytesPerFrame; - int offset = pendingBytes - size; + int offset = pendingOutputBytes - size; trackOutput.sampleMetadata( timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); outputFrameCount += pendingFrames; - pendingBytes = offset; + pendingOutputBytes = offset; } return endOfSampleData; } } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; + + /** The number of pending bytes in {@link #inputData}. */ + private int pendingInputBytes; + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); + + // Create the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java index 53e0f45306..2a92c38431 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -49,20 +49,18 @@ import com.google.android.exoplayer2.util.Util; @Override public SeekPoints getSeekPoints(long timeUs) { - // Calculate the expected number of bytes of sample data corresponding to the requested time. - long positionOffset = (timeUs * wavHeader.averageBytesPerSecond) / C.MICROS_PER_SECOND; // Calculate the containing block index, constraining to valid indices. - long blockSize = wavHeader.blockSize; - long blockIndex = Util.constrainValue(positionOffset / blockSize, 0, blockCount - 1); + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); - long seekPosition = firstBlockPosition + (blockIndex * blockSize); + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); long seekTimeUs = blockIndexToTimeUs(blockIndex); SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { return new SeekPoints(seekPoint); } else { long secondBlockIndex = blockIndex + 1; - long secondSeekPosition = firstBlockPosition + (secondBlockIndex * blockSize); + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); return new SeekPoints(seekPoint, secondSeekPoint); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java index 0d126ff27f..18c7b1c201 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapter.java @@ -33,12 +33,12 @@ import com.google.android.exoplayer2.util.Assertions; */ @RequiresApi(21) /* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { - private MediaCodecAsyncCallback mediaCodecAsyncCallback; + private final MediaCodecAsyncCallback mediaCodecAsyncCallback; private final Handler handler; private final MediaCodec codec; @Nullable private IllegalStateException internalException; private boolean flushing; - private Runnable onCodecStart; + private Runnable codecStartRunnable; /** * Create a new {@code AsynchronousMediaCodecAdapter}. @@ -51,11 +51,16 @@ import com.google.android.exoplayer2.util.Assertions; @VisibleForTesting /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { - this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); + mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); handler = new Handler(looper); this.codec = codec; this.codec.setCallback(mediaCodecAsyncCallback); - onCodecStart = () -> codec.start(); + codecStartRunnable = codec::start; + } + + @Override + public void start() { + codecStartRunnable.run(); } @Override @@ -105,7 +110,7 @@ import com.google.android.exoplayer2.util.Assertions; flushing = false; mediaCodecAsyncCallback.flush(); try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { // Catch IllegalStateException directly so that we don't have to wrap it. internalException = e; @@ -115,8 +120,8 @@ import com.google.android.exoplayer2.util.Assertions; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private void maybeThrowException() throws IllegalStateException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java index bad21f91f8..b623811453 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapter.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -54,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @MonotonicNonNull private Handler handler; private long pendingFlushCount; private @State int state; - private Runnable onCodecStart; + private Runnable codecStartRunnable; @Nullable private IllegalStateException internalException; /** @@ -77,31 +76,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.codec = codec; this.handlerThread = handlerThread; state = STATE_CREATED; - onCodecStart = codec::start; + codecStartRunnable = codec::start; } - /** - * Starts the operation of the instance. - * - *

After a call to this method, make sure to call {@link #shutdown()} to terminate the internal - * Thread. You can only call this method once during the lifetime of this instance; calling this - * method again will throw an {@link IllegalStateException}. - * - * @throws IllegalStateException If this method has been called already. - */ + @Override public synchronized void start() { - Assertions.checkState(state == STATE_CREATED); - handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codecStartRunnable.run(); state = STATE_STARTED; } @Override public synchronized int dequeueInputBufferIndex() { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -112,8 +100,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -124,15 +110,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public synchronized MediaFormat getOutputFormat() { - Assertions.checkState(state == STATE_STARTED); - return mediaCodecAsyncCallback.getOutputFormat(); } @Override public synchronized void flush() { - Assertions.checkState(state == STATE_STARTED); - codec.flush(); ++pendingFlushCount; Util.castNonNull(handler).post(this::onFlushCompleted); @@ -177,8 +159,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private synchronized void onFlushCompleted() { @@ -199,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; mediaCodecAsyncCallback.flush(); try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { internalException = e; } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java index c984443041..2f347de0ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecAdapter.java @@ -31,6 +31,13 @@ import android.media.MediaFormat; */ /* package */ interface MediaCodecAdapter { + /** + * Starts this instance. + * + * @see MediaCodec#start(). + */ + void start(); + /** * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link * MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. 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 e973b70204..89a0cb5ae1 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 @@ -995,13 +995,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD && Util.SDK_INT >= 23) { codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); - ((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start(); } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK && Util.SDK_INT >= 23) { codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); - ((MultiLockAsyncMediaCodecAdapter) codecAdapter).start(); } else { - codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); + codecAdapter = new SynchronousMediaCodecAdapter(codec); } TraceUtil.endSection(); @@ -1009,7 +1007,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); - codec.start(); + codecAdapter.start(); TraceUtil.endSection(); codecInitializedTimestamp = SystemClock.elapsedRealtime(); getCodecBuffers(codec); @@ -1460,15 +1458,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); } - /** - * Returns the maximum time to block whilst waiting for a decoded output buffer. - * - * @return The maximum time to block, in microseconds. - */ - protected long getDequeueOutputBufferTimeoutUs() { - return 0; - } - /** * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, * current {@link Format} and set of possible stream formats. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java index 56f503c71a..48d4ac9a55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapter.java @@ -27,7 +27,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.IntArrayQueue; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; @@ -94,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final HandlerThread handlerThread; @MonotonicNonNull private Handler handler; - private Runnable onCodecStart; + private Runnable codecStartRunnable; /** Creates a new instance that wraps the specified {@link MediaCodec}. */ /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { @@ -114,25 +113,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; codecException = null; state = STATE_CREATED; this.handlerThread = handlerThread; - onCodecStart = codec::start; + codecStartRunnable = codec::start; } - /** - * Starts the operation of this instance. - * - *

After a call to this method, make sure to call {@link #shutdown()} to terminate the internal - * Thread. You can only call this method once during the lifetime of an instance; calling this - * method again will throw an {@link IllegalStateException}. - * - * @throws IllegalStateException If this method has been called already. - */ + @Override public void start() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_CREATED); - handlerThread.start(); handler = new Handler(handlerThread.getLooper()); codec.setCallback(this, handler); + codecStartRunnable.run(); state = STATE_STARTED; } } @@ -140,8 +130,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueInputBufferIndex() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -154,8 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (isFlushing()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } else { @@ -168,8 +154,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public MediaFormat getOutputFormat() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - if (currentFormat == null) { throw new IllegalStateException(); } @@ -181,8 +165,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void flush() { synchronized (objectStateLock) { - Assertions.checkState(state == STATE_STARTED); - codec.flush(); pendingFlush++; Util.castNonNull(handler).post(this::onFlushComplete); @@ -200,8 +182,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @VisibleForTesting - /* package */ void setOnCodecStart(Runnable onCodecStart) { - this.onCodecStart = onCodecStart; + /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) { + this.codecStartRunnable = codecStartRunnable; } private int dequeueAvailableInputBufferIndex() { @@ -307,7 +289,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; clearAvailableOutput(); codecException = null; try { - onCodecStart.run(); + codecStartRunnable.run(); } catch (IllegalStateException e) { codecException = e; } catch (Exception e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java index 8caf72ecf4..ee9ab857cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/SynchronousMediaCodecAdapter.java @@ -23,12 +23,16 @@ import android.media.MediaFormat; * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. */ /* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { - private final MediaCodec codec; - private final long dequeueOutputBufferTimeoutMs; - public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) { + private final MediaCodec codec; + + public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { this.codec = mediaCodec; - this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; + } + + @Override + public void start() { + codec.start(); } @Override @@ -38,7 +42,7 @@ import android.media.MediaFormat; @Override public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { - return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); + return codec.dequeueOutputBuffer(bufferInfo, 0); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index ff95afb1f6..1641b2aef6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -773,7 +773,7 @@ public final class DownloadHelper { } // Initialization of array of Lists. - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private void onMediaPrepared() { Assertions.checkNotNull(mediaPreparer); Assertions.checkNotNull(mediaPreparer.mediaPeriods); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 87ea60bf73..8919a26720 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -165,7 +165,7 @@ public final class Requirements implements Parcelable { private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { // It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only // fires an event to update its Requirements when NetworkCapabilities change from API level 24. - // Since Requirements wont be updated, we assume connectivity is validated on API level 23. + // Since Requirements won't be updated, we assume connectivity is validated on API level 23. if (Util.SDK_INT < 24) { return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java new file mode 100644 index 0000000000..9e9f350dd7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SpanUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text; + +import android.text.Spannable; +import android.text.style.ForegroundColorSpan; + +/** + * Utility methods for Android span + * styling. + */ +public final class SpanUtil { + + /** + * Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any + * existing spans of the same type and with the same indices and flags. + * + *

This is useful for types of spans that don't make sense to duplicate and where the + * evaluation order might have an unexpected impact on the final text, e.g. {@link + * ForegroundColorSpan}. + * + * @param spannable The {@link Spannable} to add {@code span} to. + * @param span The span object to be added. + * @param start The start index to add the new span at. + * @param end The end index to add the new span at. + * @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}. + */ + public static void addOrReplaceSpan( + Spannable spannable, Object span, int start, int end, int spanFlags) { + Object[] existingSpans = spannable.getSpans(start, end, span.getClass()); + for (Object existingSpan : existingSpans) { + if (spannable.getSpanStart(existingSpan) == start + && spannable.getSpanEnd(existingSpan) == end + && spannable.getSpanFlags(existingSpan) == spanFlags) { + spannable.removeSpan(existingSpan); + } + } + spannable.setSpan(span, start, end, spanFlags); + } + + private SpanUtil() {} +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 228973ce0c..8d99816ee1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -481,8 +481,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * * @return The parsed object data. */ - // incompatible types in argument. - @SuppressWarnings("nullness:argument.type.incompatible") private static ObjectData parseObjectData(ParsableBitArray data) { int objectId = data.readBits(16); data.skipBits(4); // Skip object_version_number @@ -490,8 +488,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - @Nullable byte[] topFieldData = null; - @Nullable byte[] bottomFieldData = null; + byte[] topFieldData = Util.EMPTY_BYTE_ARRAY; + byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java new file mode 100644 index 0000000000..587e1647c6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/HorizontalTextInVerticalContextSpan.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.span; + +/** + * A styling span for horizontal text in a vertical context. + * + *

This is used in vertical text to write some characters in a horizontal orientation, known in + * Japanese as tate-chu-yoko. + * + *

More information on tate-chu-yoko and span styling. + */ +// NOTE: There's no Android layout support for this, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to +// extract the spans and do the layout manually. +public final class HorizontalTextInVerticalContextSpan {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java new file mode 100644 index 0000000000..8ed84d6f6b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/span/RubySpan.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.span; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import androidx.annotation.IntDef; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** + * A styling span for ruby text. + * + *

The text covered by this span is known as the "base text", and the ruby text is stored in + * {@link #rubyText}. + * + *

More information on ruby characters + * and span styling. + */ +// NOTE: There's no Android layout support for rubies, so this span currently doesn't extend any +// styling superclasses (e.g. MetricAffectingSpan). The only way to render these rubies is to +// extract the spans and do the layout manually. +// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support +// rubies (e.g. HTML tag). +public final class RubySpan { + + /** The ruby position is unknown. */ + public static final int POSITION_UNKNOWN = -1; + + /** + * The ruby text should be positioned above the base text. + * + *

For vertical text it should be positioned to the right, same as CSS's ruby-position. + */ + public static final int POSITION_OVER = 1; + + /** + * The ruby text should be positioned below the base text. + * + *

For vertical text it should be positioned to the left, same as CSS's ruby-position. + */ + public static final int POSITION_UNDER = 2; + + /** + * The possible positions of the ruby text relative to the base text. + * + *

One of: + * + *

    + *
  • {@link #POSITION_UNKNOWN} + *
  • {@link #POSITION_OVER} + *
  • {@link #POSITION_UNDER} + *
+ */ + @Documented + @Retention(SOURCE) + @IntDef({POSITION_UNKNOWN, POSITION_OVER, POSITION_UNDER}) + public @interface Position {} + + /** The ruby text, i.e. the smaller explanatory characters. */ + public final String rubyText; + + /** The position of the ruby text relative to the base text. */ + @Position public final int position; + + public RubySpan(String rubyText, @Position int position) { + this.rubyText = rubyText; + this.position = position; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java index 21333081c6..25395431de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.text.ttml; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.AbsoluteSizeSpan; @@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.text.SpanUtil; import java.util.Map; /** @@ -77,32 +77,60 @@ import java.util.Map; builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpanUtil.addOrReplaceSpan( + builder, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { - builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + SpanUtil.addOrReplaceSpan( + builder, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new TypefaceSpan(style.getFontFamily()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getTextAlign() != null) { - builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AlignmentSpan.Standard(style.getTextAlign()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case TtmlStyle.FONT_SIZE_UNIT_PIXEL: - builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_EM: - builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.FONT_SIZE_UNIT_PERCENT: - builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + SpanUtil.addOrReplaceSpan( + builder, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TtmlStyle.UNSPECIFIED: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index 9a5ac40a05..7d5d51b706 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -31,14 +31,19 @@ import java.util.regex.Pattern; */ /* package */ final class CssParser { + private static final String TAG = "CssParser"; + + private static final String RULE_START = "{"; + private static final String RULE_END = "}"; private static final String PROPERTY_BGCOLOR = "background-color"; private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_WEIGHT = "font-weight"; + private static final String PROPERTY_TEXT_COMBINE_UPRIGHT = "text-combine-upright"; + private static final String VALUE_ALL = "all"; + private static final String VALUE_DIGITS = "digits"; private static final String PROPERTY_TEXT_DECORATION = "text-decoration"; private static final String VALUE_BOLD = "bold"; private static final String VALUE_UNDERLINE = "underline"; - private static final String RULE_START = "{"; - private static final String RULE_END = "}"; private static final String PROPERTY_FONT_STYLE = "font-style"; private static final String VALUE_ITALIC = "italic"; @@ -182,6 +187,8 @@ import java.util.regex.Pattern; style.setFontColor(ColorParser.parseCssColor(value)); } else if (PROPERTY_BGCOLOR.equals(property)) { style.setBackgroundColor(ColorParser.parseCssColor(value)); + } else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) { + style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS)); } else if (PROPERTY_TEXT_DECORATION.equals(property)) { if (VALUE_UNDERLINE.equals(value)) { style.setUnderline(true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index 97c0acb1ec..cd08ad18cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -95,6 +95,7 @@ public final class WebvttCssStyle { @FontSizeUnit private int fontSizeUnit; private float fontSize; @Nullable private Layout.Alignment textAlign; + private boolean combineUpright; // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed // because reset() only assigns fields, it doesn't read any. @@ -118,6 +119,7 @@ public final class WebvttCssStyle { italic = UNSPECIFIED; fontSizeUnit = UNSPECIFIED; textAlign = null; + combineUpright = false; } public void setTargetId(String targetId) { @@ -287,35 +289,12 @@ public final class WebvttCssStyle { return fontSize; } - public void cascadeFrom(WebvttCssStyle style) { - if (style.hasFontColor) { - setFontColor(style.fontColor); - } - if (style.bold != UNSPECIFIED) { - bold = style.bold; - } - if (style.italic != UNSPECIFIED) { - italic = style.italic; - } - if (style.fontFamily != null) { - fontFamily = style.fontFamily; - } - if (linethrough == UNSPECIFIED) { - linethrough = style.linethrough; - } - if (underline == UNSPECIFIED) { - underline = style.underline; - } - if (textAlign == null) { - textAlign = style.textAlign; - } - if (fontSizeUnit == UNSPECIFIED) { - fontSizeUnit = style.fontSizeUnit; - fontSize = style.fontSize; - } - if (style.hasBackgroundColor) { - setBackgroundColor(style.backgroundColor); - } + public void setCombineUpright(boolean enabled) { + this.combineUpright = enabled; + } + + public boolean getCombineUpright() { + return combineUpright; } private static int updateScoreForMatch( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 3a07a74042..f62b073f60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -15,11 +15,11 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.graphics.Typeface; import android.text.Layout; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; @@ -37,6 +37,8 @@ import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -120,11 +122,13 @@ public final class WebvttCueParser { private static final String ENTITY_NON_BREAK_SPACE = "nbsp"; private static final String TAG_BOLD = "b"; - private static final String TAG_ITALIC = "i"; - private static final String TAG_UNDERLINE = "u"; private static final String TAG_CLASS = "c"; - private static final String TAG_VOICE = "v"; + private static final String TAG_ITALIC = "i"; private static final String TAG_LANG = "lang"; + private static final String TAG_RUBY = "ruby"; + private static final String TAG_RUBY_TEXT = "rt"; + private static final String TAG_UNDERLINE = "u"; + private static final String TAG_VOICE = "v"; private static final int STYLE_BOLD = Typeface.BOLD; private static final int STYLE_ITALIC = Typeface.ITALIC; @@ -197,6 +201,7 @@ public final class WebvttCueParser { ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; + List nestedElements = new ArrayList<>(); while (pos < markup.length()) { char curr = markup.charAt(pos); switch (curr) { @@ -225,8 +230,14 @@ public final class WebvttCueParser { break; } startTag = startTagStack.pop(); - applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches); - } while(!startTag.name.equals(tagName)); + applySpansForTag( + id, startTag, nestedElements, spannedText, styles, scratchStyleMatches); + if (!startTagStack.isEmpty()) { + nestedElements.add(new Element(startTag, spannedText.length())); + } else { + nestedElements.clear(); + } + } while (!startTag.name.equals(tagName)); } else if (!isVoidTag) { startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length())); } @@ -256,9 +267,15 @@ public final class WebvttCueParser { } // apply unclosed tags while (!startTagStack.isEmpty()) { - applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches); + applySpansForTag( + id, startTagStack.pop(), nestedElements, spannedText, styles, scratchStyleMatches); } - applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles, + applySpansForTag( + id, + StartTag.buildWholeCueVirtualTag(), + /* nestedElements= */ Collections.emptyList(), + spannedText, + styles, scratchStyleMatches); return SpannedString.valueOf(spannedText); } @@ -442,6 +459,8 @@ public final class WebvttCueParser { case TAG_CLASS: case TAG_ITALIC: case TAG_LANG: + case TAG_RUBY: + case TAG_RUBY_TEXT: case TAG_UNDERLINE: case TAG_VOICE: return true; @@ -453,6 +472,7 @@ public final class WebvttCueParser { private static void applySpansForTag( @Nullable String cueId, StartTag startTag, + List nestedElements, SpannableStringBuilder text, List styles, List scratchStyleMatches) { @@ -467,6 +487,29 @@ public final class WebvttCueParser { text.setSpan(new StyleSpan(STYLE_ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; + case TAG_RUBY: + @Nullable Element rubyTextElement = null; + for (int i = 0; i < nestedElements.size(); i++) { + if (TAG_RUBY_TEXT.equals(nestedElements.get(i).startTag.name)) { + rubyTextElement = nestedElements.get(i); + // Behaviour of multiple tags inside is undefined, so use the first one. + break; + } + } + if (rubyTextElement == null) { + break; + } + // Move the rubyText from spannedText into the RubySpan. + CharSequence rubyText = + text.subSequence(rubyTextElement.startTag.position, rubyTextElement.endPosition); + text.delete(rubyTextElement.startTag.position, rubyTextElement.endPosition); + end -= rubyText.length(); + text.setSpan( + new RubySpan(rubyText.toString(), RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; case TAG_UNDERLINE: text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; @@ -492,7 +535,11 @@ public final class WebvttCueParser { return; } if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { - spannedText.setSpan(new StyleSpan(style.getStyle()), start, end, + addOrReplaceSpan( + spannedText, + new StyleSpan(style.getStyle()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.isLinethrough()) { @@ -502,39 +549,71 @@ public final class WebvttCueParser { spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasFontColor()) { - spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, + new ForegroundColorSpan(style.getFontColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.hasBackgroundColor()) { - spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, + new BackgroundColorSpan(style.getBackgroundColor()), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if (style.getFontFamily() != null) { - spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + addOrReplaceSpan( + spannedText, + new TypefaceSpan(style.getFontFamily()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } Layout.Alignment textAlign = style.getTextAlign(); if (textAlign != null) { - spannedText.setSpan( - new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + addOrReplaceSpan( + spannedText, + new AlignmentSpan.Standard(textAlign), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: - spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + addOrReplaceSpan( + spannedText, + new AbsoluteSizeSpan((int) style.getFontSize(), true), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_EM: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize()), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: - spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + addOrReplaceSpan( + spannedText, + new RelativeSizeSpan(style.getFontSize() / 100), + start, + end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case WebvttCssStyle.UNSPECIFIED: // Do nothing. break; } + if (style.getCombineUpright()) { + spannedText.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } } /** @@ -773,4 +852,19 @@ public final class WebvttCueParser { } } + + /** Information about a complete element (i.e. start tag and end position). */ + private static class Element { + private final StartTag startTag; + /** + * The position of the end of this element's text in the un-marked-up cue text (i.e. the + * corollary to {@link StartTag#position}). + */ + private final int endPosition; + + private Element(StartTag startTag, int endPosition) { + this.startTag = startTag; + this.endPosition = endPosition; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java deleted file mode 100644 index b850a08aeb..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.trackselection; - -import android.util.Pair; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; - -/** - * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size - * based track adaptation. - */ -public final class BufferSizeAdaptationBuilder { - - /** Dynamic filter for formats, which is applied when selecting a new track. */ - public interface DynamicFormatFilter { - - /** Filter which allows all formats. */ - DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; - - /** - * Called when updating the selected track to determine whether a candidate track is allowed. If - * no format is allowed or eligible, the lowest quality format will be used. - * - * @param format The {@link Format} of the candidate track. - * @param trackBitrate The estimated bitrate of the track. May differ from {@link - * Format#bitrate} if a more accurate estimate of the current track bitrate is available. - * @param isInitialSelection Whether this is for the initial track selection. - */ - boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); - } - - /** - * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. - */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; - - /** - * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - */ - public static final int DEFAULT_MAX_BUFFER_MS = 50000; - - /** - * The default duration of media that must be buffered for playback to start or resume following a - * user action such as a seek, in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; - - /** - * The default duration of media that must be buffered for playback to resume after a rebuffer, in - * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - - /** - * The default offset the current duration of buffered media must deviate from the ideal duration - * of buffered media for the currently selected format, before the selected format is changed. - */ - public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000; - - /** - * During start-up phase, the default fraction of the available bandwidth that the selection - * should consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - */ - public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION = - AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION; - - /** - * During start-up phase, the default minimum duration of buffered media required for the selected - * track to switch to one of higher quality based on measured bandwidth. - */ - public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS = - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; - - @Nullable private DefaultAllocator allocator; - private Clock clock; - private int minBufferMs; - private int maxBufferMs; - private int bufferForPlaybackMs; - private int bufferForPlaybackAfterRebufferMs; - private int hysteresisBufferMs; - private float startUpBandwidthFraction; - private int startUpMinBufferForQualityIncreaseMs; - private DynamicFormatFilter dynamicFormatFilter; - private boolean buildCalled; - - /** Creates builder with default values. */ - public BufferSizeAdaptationBuilder() { - clock = Clock.DEFAULT; - minBufferMs = DEFAULT_MIN_BUFFER_MS; - maxBufferMs = DEFAULT_MAX_BUFFER_MS; - bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS; - bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS; - startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION; - startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS; - dynamicFormatFilter = DynamicFormatFilter.NO_FILTER; - } - - /** - * Set the clock to use. Should only be set for testing purposes. - * - * @param clock The {@link Clock}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setClock(Clock clock) { - Assertions.checkState(!buildCalled); - this.clock = clock; - return this; - } - - /** - * Sets the {@link DefaultAllocator} used by the loader. - * - * @param allocator The {@link DefaultAllocator}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) { - Assertions.checkState(!buildCalled); - this.allocator = allocator; - return this; - } - - /** - * Sets the buffer duration parameters. - * - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - * buffer depletion rather than a user action. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setBufferDurationsMs( - int minBufferMs, - int maxBufferMs, - int bufferForPlaybackMs, - int bufferForPlaybackAfterRebufferMs) { - Assertions.checkState(!buildCalled); - this.minBufferMs = minBufferMs; - this.maxBufferMs = maxBufferMs; - this.bufferForPlaybackMs = bufferForPlaybackMs; - this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs; - return this; - } - - /** - * Sets the hysteresis buffer used to prevent repeated format switching. - * - * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from - * the ideal duration of buffered media for the currently selected format, before the selected - * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) { - Assertions.checkState(!buildCalled); - this.hysteresisBufferMs = hysteresisBufferMs; - return this; - } - - /** - * Sets track selection parameters used during the start-up phase before the selection can be made - * purely on based on buffer size. During the start-up phase the selection is based on the current - * bandwidth estimate. - * - * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account for - * inaccuracies in the bandwidth estimator. - * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the - * selected track to switch to one of higher quality. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters( - float bandwidthFraction, int minBufferForQualityIncreaseMs) { - Assertions.checkState(!buildCalled); - this.startUpBandwidthFraction = bandwidthFraction; - this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs; - return this; - } - - /** - * Sets the {@link DynamicFormatFilter} to use when updating the selected track. - * - * @param dynamicFormatFilter The {@link DynamicFormatFilter}. - * @return This builder, for convenience. - * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called. - */ - public BufferSizeAdaptationBuilder setDynamicFormatFilter( - DynamicFormatFilter dynamicFormatFilter) { - Assertions.checkState(!buildCalled); - this.dynamicFormatFilter = dynamicFormatFilter; - return this; - } - - /** - * Builds player components for buffer size based track adaptation. - * - * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be - * used to construct the player. - */ - public Pair buildPlayerComponents() { - Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs); - Assertions.checkState(!buildCalled); - buildCalled = true; - - DefaultLoadControl.Builder loadControlBuilder = - new DefaultLoadControl.Builder() - .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE) - .setBufferDurationsMs( - /* minBufferMs= */ maxBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs); - if (allocator != null) { - loadControlBuilder.setAllocator(allocator); - } - - TrackSelection.Factory trackSelectionFactory = - new TrackSelection.Factory() { - @Override - public @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> - new BufferSizeAdaptiveTrackSelection( - definition.group, - definition.tracks, - bandwidthMeter, - minBufferMs, - maxBufferMs, - hysteresisBufferMs, - startUpBandwidthFraction, - startUpMinBufferForQualityIncreaseMs, - dynamicFormatFilter, - clock)); - } - }; - - return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl()); - } - - private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection { - - private static final int BITRATE_BLACKLISTED = Format.NO_VALUE; - - private final BandwidthMeter bandwidthMeter; - private final Clock clock; - private final DynamicFormatFilter dynamicFormatFilter; - private final int[] formatBitrates; - private final long minBufferUs; - private final long maxBufferUs; - private final long hysteresisBufferUs; - private final float startUpBandwidthFraction; - private final long startUpMinBufferForQualityIncreaseUs; - private final int minBitrate; - private final int maxBitrate; - private final double bitrateToBufferFunctionSlope; - private final double bitrateToBufferFunctionIntercept; - - private boolean isInSteadyState; - private int selectedIndex; - private int selectionReason; - private float playbackSpeed; - - private BufferSizeAdaptiveTrackSelection( - TrackGroup trackGroup, - int[] tracks, - BandwidthMeter bandwidthMeter, - int minBufferMs, - int maxBufferMs, - int hysteresisBufferMs, - float startUpBandwidthFraction, - int startUpMinBufferForQualityIncreaseMs, - DynamicFormatFilter dynamicFormatFilter, - Clock clock) { - super(trackGroup, tracks); - this.bandwidthMeter = bandwidthMeter; - this.minBufferUs = C.msToUs(minBufferMs); - this.maxBufferUs = C.msToUs(maxBufferMs); - this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs); - this.startUpBandwidthFraction = startUpBandwidthFraction; - this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs); - this.dynamicFormatFilter = dynamicFormatFilter; - this.clock = clock; - - formatBitrates = new int[length]; - maxBitrate = getFormat(/* index= */ 0).bitrate; - minBitrate = getFormat(/* index= */ length - 1).bitrate; - selectionReason = C.SELECTION_REASON_UNKNOWN; - playbackSpeed = 1.0f; - - // We use a log-linear function to map from bitrate to buffer size: - // buffer = slope * ln(bitrate) + intercept, - // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer. - bitrateToBufferFunctionSlope = - (maxBufferUs - hysteresisBufferUs - minBufferUs) - / Math.log((double) maxBitrate / minBitrate); - bitrateToBufferFunctionIntercept = - minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); - } - - @Override - public void onPlaybackSpeed(float playbackSpeed) { - this.playbackSpeed = playbackSpeed; - } - - @Override - public void onDiscontinuity() { - isInSteadyState = false; - } - - @Override - public int getSelectedIndex() { - return selectedIndex; - } - - @Override - public int getSelectionReason() { - return selectionReason; - } - - @Override - @Nullable - public Object getSelectionData() { - return null; - } - - @Override - public void updateSelectedTrack( - long playbackPositionUs, - long bufferedDurationUs, - long availableDurationUs, - List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); - - // Make initial selection - if (selectionReason == C.SELECTION_REASON_UNKNOWN) { - selectionReason = C.SELECTION_REASON_INITIAL; - selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); - return; - } - - long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); - int oldSelectedIndex = selectedIndex; - if (isInSteadyState) { - selectIndexSteadyState(bufferUs); - } else { - selectIndexStartUpPhase(bufferUs); - } - if (selectedIndex != oldSelectedIndex) { - selectionReason = C.SELECTION_REASON_ADAPTIVE; - } - } - - // Steady state. - - private void selectIndexSteadyState(long bufferUs) { - if (isOutsideHysteresis(bufferUs)) { - selectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - } - } - - private boolean isOutsideHysteresis(long bufferUs) { - if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) { - return true; - } - long targetBufferForCurrentBitrateUs = - getTargetBufferForBitrateUs(formatBitrates[selectedIndex]); - long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs; - return Math.abs(bufferDiffUs) > hysteresisBufferUs; - } - - private int selectIdealIndexUsingBufferSize(long bufferUs) { - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Startup. - - private void selectIndexStartUpPhase(long bufferUs) { - int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); - int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); - if (steadyStateSelectedIndex <= selectedIndex) { - // Switch to steady state if we have enough buffer to maintain current selection. - selectedIndex = steadyStateSelectedIndex; - isInSteadyState = true; - } else { - if (bufferUs < startUpMinBufferForQualityIncreaseUs - && startUpSelectedIndex < selectedIndex - && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) { - // Switching up from a non-blacklisted track is only allowed if we have enough buffer. - return; - } - selectedIndex = startUpSelectedIndex; - } - } - - private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { - long effectiveBitrate = - (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); - int lowestBitrateNonBlacklistedIndex = 0; - for (int i = 0; i < formatBitrates.length; i++) { - if (formatBitrates[i] != BITRATE_BLACKLISTED) { - if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate - && dynamicFormatFilter.isFormatAllowed( - getFormat(i), formatBitrates[i], isInitialSelection)) { - return i; - } - lowestBitrateNonBlacklistedIndex = i; - } - } - return lowestBitrateNonBlacklistedIndex; - } - - // Utility methods. - - private void updateFormatBitrates(long nowMs) { - for (int i = 0; i < length; i++) { - if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { - formatBitrates[i] = getFormat(i).bitrate; - } else { - formatBitrates[i] = BITRATE_BLACKLISTED; - } - } - } - - private long getTargetBufferForBitrateUs(int bitrate) { - if (bitrate <= minBitrate) { - return minBufferUs; - } - if (bitrate >= maxBitrate) { - return maxBufferUs - hysteresisBufferUs; - } - return (int) - (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept); - } - - private static long getCurrentPeriodBufferedDurationUs( - long playbackPositionUs, long bufferedDurationUs) { - return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs; - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 1b69455695..2491cc93a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -203,9 +203,10 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); - // Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate. + // Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback. result.append( C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); return result; } 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 54e65797f0..65ffcf351e 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 @@ -1355,6 +1355,7 @@ public final class Util { public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT || encoding == C.ENCODING_PCM_FLOAT; @@ -1423,14 +1424,13 @@ public final class Util { case C.ENCODING_PCM_8BIT: return channelCount; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: return channelCount * 2; case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_FLOAT: return channelCount * 4; - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -2126,6 +2126,8 @@ public final class Util { return C.NETWORK_TYPE_3G; case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_NR: + return C.NETWORK_TYPE_5G; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.0.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.1.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.2.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump b/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump index ac111d0c62..70b22f16aa 100644 --- a/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump +++ b/library/core/src/test/assets/mkv/full_blocks.mkv.3.dump @@ -31,13 +31,13 @@ track 1: sample 0: time = 0 flags = 1 - data = length 59, hash A0217393 + data = length 59, hash 1AD38625 sample 1: time = 2345000 flags = 1 - data = length 95, hash 4904F2 + data = length 95, hash F331C282 sample 2: time = 4567000 flags = 1 - data = length 59, hash EFAB6D8A + data = length 59, hash F8CD7C60 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4 b/library/core/src/test/assets/mp4/sample_ac4.mp4 new file mode 100644 index 0000000000..d649632c74 Binary files /dev/null and b/library/core/src/test/assets/mp4/sample_ac4.mp4 differ diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump new file mode 100644 index 0000000000..92ba157e3f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=758]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = 622 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 0 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 0 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 0 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 0 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 0 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 0 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 0 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 0 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 0 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 0 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 0 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 0 + data = length 520, hash FEE56928 + sample 13: + time = 519999 + flags = 0 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 0 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 0 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 0 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 0 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 536870912 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 new file mode 100644 index 0000000000..2056348768 Binary files /dev/null and b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4 differ diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump new file mode 100644 index 0000000000..505c85e51f --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump @@ -0,0 +1,107 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 7613 + sample count = 19 + sample 0: + time = 0 + flags = 1 + data = length 367, hash D2762FA + sample 1: + time = 40000 + flags = 1 + data = length 367, hash BDD3224A + sample 2: + time = 80000 + flags = 1 + data = length 367, hash 9302227B + sample 3: + time = 120000 + flags = 1 + data = length 367, hash 72996003 + sample 4: + time = 160000 + flags = 1 + data = length 367, hash 88AE5A1B + sample 5: + time = 200000 + flags = 1 + data = length 367, hash E5346FE3 + sample 6: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 7: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 8: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 9: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 10: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 11: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 12: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 13: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 14: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 15: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 16: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 17: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 18: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump new file mode 100644 index 0000000000..8bee343bd9 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump @@ -0,0 +1,83 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 5411 + sample count = 13 + sample 0: + time = 240000 + flags = 1 + data = length 367, hash CE558362 + sample 1: + time = 280000 + flags = 1 + data = length 367, hash 51AD3043 + sample 2: + time = 320000 + flags = 1 + data = length 367, hash EB72E95B + sample 3: + time = 360000 + flags = 1 + data = length 367, hash 47F8FF23 + sample 4: + time = 400000 + flags = 1 + data = length 367, hash 8133883D + sample 5: + time = 440000 + flags = 1 + data = length 495, hash E14BDFEE + sample 6: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 7: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 8: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 9: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 10: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 11: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 12: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump new file mode 100644 index 0000000000..ee1cf91a57 --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 3081 + sample count = 7 + sample 0: + time = 480000 + flags = 1 + data = length 520, hash FEE56928 + sample 1: + time = 520000 + flags = 1 + data = length 599, hash 41F496C5 + sample 2: + time = 560000 + flags = 1 + data = length 436, hash 76D6404 + sample 3: + time = 600000 + flags = 1 + data = length 366, hash 56D49D4D + sample 4: + time = 640000 + flags = 1 + data = length 393, hash 822FC8 + sample 5: + time = 680000 + flags = 1 + data = length 374, hash FA8AE217 + sample 6: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump new file mode 100644 index 0000000000..419f0444bf --- /dev/null +++ b/library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 760000 + getPosition(0) = [[timeUs=0, position=685]] +numberOfTracks = 1 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = audio/ac4 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 2 + sampleRate = 48000 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + metadata = null + initializationData: + total output bytes = 393 + sample count = 1 + sample 0: + time = 720000 + flags = 1 + data = length 393, hash 8506A1B +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav b/library/core/src/test/assets/wav/sample_ima_adpcm.wav new file mode 100644 index 0000000000..661d54d1d7 Binary files /dev/null and b/library/core/src/test/assets/wav/sample_ima_adpcm.wav differ diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump new file mode 100644 index 0000000000..a16ad68dfa --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump @@ -0,0 +1,75 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 89804 + sample count = 11 + sample 0: + time = 0 + flags = 1 + data = length 8820, hash E90A457C + sample 1: + time = 100000 + flags = 1 + data = length 8820, hash EA798370 + sample 2: + time = 200000 + flags = 1 + data = length 8820, hash A57ED989 + sample 3: + time = 300000 + flags = 1 + data = length 8820, hash 8B681816 + sample 4: + time = 400000 + flags = 1 + data = length 8820, hash 48177BEB + sample 5: + time = 500000 + flags = 1 + data = length 8820, hash 70197776 + sample 6: + time = 600000 + flags = 1 + data = length 8820, hash DB4A4704 + sample 7: + time = 700000 + flags = 1 + data = length 8820, hash 84A525D0 + sample 8: + time = 800000 + flags = 1 + data = length 8820, hash 197A4377 + sample 9: + time = 900000 + flags = 1 + data = length 8820, hash 6982BC91 + sample 10: + time = 1000000 + flags = 1 + data = length 1604, hash 3DED68ED +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump new file mode 100644 index 0000000000..3eb13e82bf --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump @@ -0,0 +1,59 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 61230 + sample count = 7 + sample 0: + time = 339395 + flags = 1 + data = length 8820, hash 25FCA092 + sample 1: + time = 439395 + flags = 1 + data = length 8820, hash 9400B4BE + sample 2: + time = 539395 + flags = 1 + data = length 8820, hash 5BA7E45D + sample 3: + time = 639395 + flags = 1 + data = length 8820, hash 5AC42905 + sample 4: + time = 739395 + flags = 1 + data = length 8820, hash D57059C + sample 5: + time = 839395 + flags = 1 + data = length 8820, hash DEF5C480 + sample 6: + time = 939395 + flags = 1 + data = length 8310, hash 10B3FC93 +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump new file mode 100644 index 0000000000..bef16523d4 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump @@ -0,0 +1,47 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 32656 + sample count = 4 + sample 0: + time = 678790 + flags = 1 + data = length 8820, hash DB7FF64C + sample 1: + time = 778790 + flags = 1 + data = length 8820, hash B895DFDC + sample 2: + time = 878790 + flags = 1 + data = length 8820, hash E3AB416D + sample 3: + time = 978790 + flags = 1 + data = length 6196, hash E27E175A +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump new file mode 100644 index 0000000000..085fe5e592 --- /dev/null +++ b/library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump @@ -0,0 +1,35 @@ +seekMap: + isSeekable = true + duration = 1018185 + getPosition(0) = [[timeUs=0, position=94]] +numberOfTracks = 1 +track 0: + format: + bitrate = 177004 + id = null + containerMimeType = null + sampleMimeType = audio/raw + maxInputSize = 8820 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = 2 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 4082 + sample count = 1 + sample 0: + time = 1018185 + flags = 1 + data = length 4082, hash 4CB1A490 +tracksEnded = true diff --git a/library/core/src/test/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors index 62e3348ae9..130d4a2529 100644 --- a/library/core/src/test/assets/webvtt/with_css_complex_selectors +++ b/library/core/src/test/assets/webvtt/with_css_complex_selectors @@ -1,7 +1,7 @@ WEBVTT STYLE -::cue(\n#id ){text-decoration:underline;} +::cue(#id ){text-decoration:underline;} STYLE ::cue(#id.class1.class2 ){ color: violet;} @@ -20,7 +20,7 @@ STYLE id 00:00.000 --> 00:01.001 -This should be underlined and courier and violet. +This should be underlined and courier and violet. íd 00:02.000 --> 00:02.001 @@ -31,10 +31,10 @@ _id This should be courier and bold. 00:04.000 --> 00:04.001 -This shouldn't be bold. -This should be bold. +This shouldn't be bold. +This should be bold. anId 00:05.000 --> 00:05.001 -This is specific - But this is more italic +This is specific +But this is more italic diff --git a/library/core/src/test/assets/webvtt/with_css_text_combine_upright b/library/core/src/test/assets/webvtt/with_css_text_combine_upright new file mode 100644 index 0000000000..fd198a9c71 --- /dev/null +++ b/library/core/src/test/assets/webvtt/with_css_text_combine_upright @@ -0,0 +1,18 @@ +WEBVTT + +NOTE https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright +NOTE The `digits` values are ignored in CssParser and all assumed to be `all` + +STYLE +::cue(.tcu-all) { + text-combine-upright: all; +} +::cue(.tcu-digits) { + text-combine-upright: digits 4; +} + +00:00:00.000 --> 00:00:01.000 vertical:rl +Combine all test + +00:03.000 --> 00:04.000 vertical:rl +Combine 0004 digits diff --git a/library/core/src/test/assets/webvtt/with_positioning b/library/core/src/test/assets/webvtt/with_positioning index 6bb86b7c93..7db327ca62 100644 --- a/library/core/src/test/assets/webvtt/with_positioning +++ b/library/core/src/test/assets/webvtt/with_positioning @@ -8,12 +8,12 @@ This is the first subtitle. NOTE Wrong position provided. It should be provided as a percentage value -00:02.345 --> 00:03.456 position:10 align:end size:35% +00:02.345 --> 00:03.456 position:10 align:end This is the second subtitle. NOTE Line as percentage and line alignment -00:04.000 --> 00:05.000 line:45%,end align:middle size:35% +00:04.000 --> 00:05.000 line:45%,end align:middle This is the third subtitle. NOTE Line as absolute negative number and without line alignment. @@ -23,10 +23,10 @@ This is the fourth subtitle. NOTE The position and positioning alignment should be inherited from align. -00:07.000 --> 00:08.000 align:right +00:08.000 --> 00:09.000 align:right This is the fifth subtitle. NOTE In newer drafts, align:middle has been replaced by align:center -00:10.000 --> 00:11.000 line:45%,end align:center size:35% +00:10.000 --> 00:11.000 align:center This is the sixth subtitle. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java new file mode 100644 index 0000000000..87487a4199 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacFrameReaderTest.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacFrameReader}. + * + *

Some expected results in these tests have been retrieved using the flac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacFrameReaderTest { + + @Test + public void checkAndReadFrameHeader_validData_updatesPosition() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(scratch.getPosition()).isEqualTo(FlacConstants.MIN_FRAME_HEADER_SIZE); + } + + @Test + public void checkAndReadFrameHeader_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndReadFrameHeader_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkAndReadFrameHeader( + scratch, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkAndReadFrameHeader_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + input.read(scratch.data, 0, FlacConstants.MAX_FRAME_HEADER_SIZE); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkAndReadFrameHeader( + scratch, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void checkFrameHeaderFromPeek_validData_isTrue() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + frameStartMarker, + new SampleNumberHolder()); + + assertThat(result).isTrue(); + } + + @Test + public void checkFrameHeaderFromPeek_validData_writesSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + // Skip first frame. + input.skip(5030); + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + + FlacFrameReader.checkFrameHeaderFromPeek( + input, streamMetadataHolder.flacStreamMetadata, frameStartMarker, sampleNumberHolder); + + assertThat(sampleNumberHolder.sampleNumber).isEqualTo(4096); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_isFalse() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + + // The first bytes of the frame are not equal to the frame start marker. + boolean result = + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(result).isFalse(); + } + + @Test + public void checkFrameHeaderFromPeek_invalidData_doesNotUpdatePositions() throws Exception { + String file = "flac/bear_one_metadata_block.flac"; + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = buildExtractorInputReadingFromFirstFrame(file, streamMetadataHolder); + long peekPosition = input.getPosition(); + // Set read position to 0. + input = buildExtractorInput(file); + input.advancePeekPosition((int) peekPosition); + + // The first bytes of the frame are not equal to the frame start marker. + FlacFrameReader.checkFrameHeaderFromPeek( + input, + streamMetadataHolder.flacStreamMetadata, + /* frameStartMarker= */ -1, + new SampleNumberHolder()); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(peekPosition); + } + + @Test + public void getFirstSampleNumber_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + long initialReadPosition = input.getPosition(); + // Advance peek position after block size bits. + input.advancePeekPosition(FlacConstants.MAX_FRAME_HEADER_SIZE); + + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(input.getPosition()).isEqualTo(initialReadPosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFirstSampleNumber_returnsSampleNumber() throws Exception { + FlacStreamMetadataHolder streamMetadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + ExtractorInput input = + buildExtractorInputReadingFromFirstFrame( + "flac/bear_one_metadata_block.flac", streamMetadataHolder); + // Skip first frame. + input.skip(5030); + + long result = + FlacFrameReader.getFirstSampleNumber(input, streamMetadataHolder.flacStreamMetadata); + + assertThat(result).isEqualTo(4096); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyIs1_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 1); + + assertThat(result).isEqualTo(192); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween2and5_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 3); + + assertThat(result).isEqualTo(1152); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween6And7_returnsCorrectBlockSize() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + // Skip to block size bits of last frame. + input.skipFully(164033); + ParsableByteArray scratch = new ParsableByteArray(2); + input.readFully(scratch.data, 0, 2); + + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(scratch, /* blockSizeKey= */ 7); + + assertThat(result).isEqualTo(496); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_keyBetween8and15_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 11); + + assertThat(result).isEqualTo(2048); + } + + @Test + public void readFrameBlockSizeSamplesFromKey_invalidKey_returnsCorrectBlockSize() { + int result = + FlacFrameReader.readFrameBlockSizeSamplesFromKey( + new ParsableByteArray(/* limit= */ 0), /* blockSizeKey= */ 25); + + assertThat(result).isEqualTo(-1); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private ExtractorInput buildExtractorInputReadingFromFirstFrame( + String file, FlacStreamMetadataHolder streamMetadataHolder) + throws IOException, InterruptedException { + ExtractorInput input = buildExtractorInput(file); + + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean lastMetadataBlock = false; + while (!lastMetadataBlock) { + lastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, streamMetadataHolder); + } + + return input; + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java new file mode 100644 index 0000000000..390e806807 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/FlacMetadataReaderTest.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMetadataHolder; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link FlacMetadataReader}. + * + *

Most expected results in these tests have been retrieved using the metaflac command. + */ +@RunWith(AndroidJUnit4.class) +public class FlacMetadataReaderTest { + + @Test + public void peekId3Metadata_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void peekId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void peekId3Metadata_noId3Metadata_returnsNull() throws Exception { + String fileWithoutId3Metadata = "flac/bear.flac"; + ExtractorInput input = buildExtractorInput(fileWithoutId3Metadata); + + Metadata metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void checkAndPeekStreamMarker_updatesPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + } + + @Test + public void checkAndPeekStreamMarker_validData_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isTrue(); + } + + @Test + public void checkAndPeekStreamMarker_invalidData_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear.mp3"); + + boolean result = FlacMetadataReader.checkAndPeekStreamMarker(input); + + assertThat(result).isFalse(); + } + + @Test + public void readId3Metadata_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + // Advance peek position after ID3 metadata. + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + input.advancePeekPosition(1); + + FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readId3Metadata_parseData_returnsNonEmptyMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNotNull(); + assertThat(metadata.length()).isNotEqualTo(0); + } + + @Test + public void readId3Metadata_doNotParseData_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_id3_enabled.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ false); + + assertThat(metadata).isNull(); + } + + @Test + public void readId3Metadata_noId3Metadata_returnsNull() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + Metadata metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ true); + + assertThat(metadata).isNull(); + } + + @Test + public void readStreamMarker_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + FlacMetadataReader.readStreamMarker(input); + + assertThat(input.getPosition()).isEqualTo(FlacConstants.STREAM_MARKER_SIZE); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test(expected = ParserException.class) + public void readStreamMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("mp3/bear.mp3"); + + FlacMetadataReader.readStreamMarker(input); + } + + @Test + public void readMetadataBlock_updatesReadPositionAndAlignsPeekPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + // Advance peek position after metadata block. + input.advancePeekPosition(FlacConstants.STREAM_INFO_BLOCK_SIZE + 1); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(input.getPosition()).isNotEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void readMetadataBlock_lastMetadataBlock_isTrue() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_one_metadata_block.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isTrue(); + } + + @Test + public void readMetadataBlock_notLastMetadataBlock_isFalse() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + + boolean result = + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + + assertThat(result).isFalse(); + } + + @Test + public void readMetadataBlock_streamInfoBlock_setsStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + input.skipFully(FlacConstants.STREAM_MARKER_SIZE); + FlacStreamMetadataHolder metadataHolder = + new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(48000); + } + + @Test + public void readMetadataBlock_seekTableBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + assertThat(metadataHolder.flacStreamMetadata.seekTable).isNotNull(); + assertThat(metadataHolder.flacStreamMetadata.seekTable.pointSampleNumbers.length).isEqualTo(32); + } + + @Test + public void readMetadataBlock_vorbisCommentBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_vorbis_comments.flac"); + // Skip to Vorbis comment block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + VorbisComment vorbisComment = (VorbisComment) metadata.get(0); + assertThat(vorbisComment.key).isEqualTo("TITLE"); + assertThat(vorbisComment.value).isEqualTo("test title"); + } + + @Test + public void readMetadataBlock_pictureBlock_updatesStreamMetadata() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear_with_picture.flac"); + // Skip to picture block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + long originalSampleRate = metadataHolder.flacStreamMetadata.sampleRate; + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(metadataHolder.flacStreamMetadata).isNotNull(); + // Check that metadata passed has not been erased. + assertThat(metadataHolder.flacStreamMetadata.sampleRate).isEqualTo(originalSampleRate); + Metadata metadata = + metadataHolder.flacStreamMetadata.getMetadataCopyWithAppendedEntriesFrom(null); + assertThat(metadata).isNotNull(); + PictureFrame pictureFrame = (PictureFrame) metadata.get(0); + assertThat(pictureFrame.pictureType).isEqualTo(3); + assertThat(pictureFrame.mimeType).isEqualTo("image/png"); + assertThat(pictureFrame.description).isEqualTo(""); + assertThat(pictureFrame.width).isEqualTo(371); + assertThat(pictureFrame.height).isEqualTo(320); + assertThat(pictureFrame.depth).isEqualTo(24); + assertThat(pictureFrame.colors).isEqualTo(0); + assertThat(pictureFrame.pictureData).hasLength(30943); + } + + @Test + public void readMetadataBlock_blockToSkip_updatesReadPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to padding block. + input.skipFully(640); + FlacStreamMetadataHolder metadataHolder = new FlacStreamMetadataHolder(buildStreamMetadata()); + + FlacMetadataReader.readMetadataBlock(input, metadataHolder); + + assertThat(input.getPosition()).isGreaterThan(640); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test(expected = IllegalArgumentException.class) + public void readMetadataBlock_nonStreamInfoBlockWithNullStreamMetadata_throwsException() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacMetadataReader.readMetadataBlock( + input, new FlacStreamMetadataHolder(/* flacStreamMetadata= */ null)); + } + + @Test + public void readSeekTableMetadataBlock_updatesPosition() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(scratch.getPosition()).isEqualTo(seekTableBlockSize); + } + + @Test + public void readSeekTableMetadataBlock_returnsCorrectSeekPoints() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to seek table block. + input.skipFully(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + int seekTableBlockSize = 598; + ParsableByteArray scratch = new ParsableByteArray(seekTableBlockSize); + input.read(scratch.data, 0, seekTableBlockSize); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + assertThat(seekTable.pointOffsets[0]).isEqualTo(0); + assertThat(seekTable.pointSampleNumbers[0]).isEqualTo(0); + assertThat(seekTable.pointOffsets[31]).isEqualTo(160602); + assertThat(seekTable.pointSampleNumbers[31]).isEqualTo(126976); + } + + @Test + public void readSeekTableMetadataBlock_ignoresPlaceholders() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + ParsableByteArray scratch = new ParsableByteArray(fileData); + // Skip to seek table block. + scratch.skipBytes(FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE); + + FlacStreamMetadata.SeekTable seekTable = FlacMetadataReader.readSeekTableMetadataBlock(scratch); + + // Seek point at index 32 is a placeholder. + assertThat(seekTable.pointSampleNumbers).hasLength(32); + } + + @Test + public void getFrameStartMarker_doesNotUpdateReadPositionAndAlignsPeekPosition() + throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + int firstFramePosition = 8880; + input.skipFully(firstFramePosition); + // Advance the peek position after the frame start marker. + input.advancePeekPosition(3); + + FlacMetadataReader.getFrameStartMarker(input); + + assertThat(input.getPosition()).isEqualTo(firstFramePosition); + assertThat(input.getPeekPosition()).isEqualTo(input.getPosition()); + } + + @Test + public void getFrameStartMarker_returnsCorrectFrameStartMarker() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + // Skip to first frame. + input.skipFully(8880); + + int result = FlacMetadataReader.getFrameStartMarker(input); + + assertThat(result).isEqualTo(0xFFF8); + } + + @Test(expected = ParserException.class) + public void getFrameStartMarker_invalidData_throwsException() throws Exception { + ExtractorInput input = buildExtractorInput("flac/bear.flac"); + + // Input position is incorrect. + FlacMetadataReader.getFrameStartMarker(input); + } + + private static ExtractorInput buildExtractorInput(String file) throws IOException { + byte[] fileData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), file); + return new FakeExtractorInput.Builder().setData(fileData).build(); + } + + private static FlacStreamMetadata buildStreamMetadata() { + return new FlacStreamMetadata( + /* minBlockSizeSamples= */ 10, + /* maxBlockSizeSamples= */ 20, + /* minFrameSize= */ 5, + /* maxFrameSize= */ 10, + /* sampleRate= */ 44100, + /* channels= */ 2, + /* bitsPerSample= */ 8, + /* totalSamples= */ 1000, + /* vorbisComments= */ new ArrayList<>(), + /* pictureFrames= */ new ArrayList<>()); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java index 97bfc949de..061d0902b6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java @@ -15,13 +15,8 @@ */ package com.google.android.exoplayer2.extractor.flac; -import android.content.Context; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; -import com.google.android.exoplayer2.testutil.TestUtil; -import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,9 +61,7 @@ public class FlacExtractorTest { @Test public void testOneMetadataBlock() throws Exception { - // Don't simulate IO errors as it is too slow when using the binary search seek map (see - // [Internal: b/145994869]). - assertBehaviorWithoutSimulatingIOErrors("flac/bear_one_metadata_block.flac"); + ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac"); } @Test @@ -85,61 +78,4 @@ public class FlacExtractorTest { public void testUncommonSampleRate() throws Exception { ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac"); } - - private static void assertBehaviorWithoutSimulatingIOErrors(String file) - throws IOException, InterruptedException { - // Check behavior prior to initialization. - Extractor extractor = new FlacExtractor(); - extractor.seek(0, 0); - extractor.release(); - - // Assert output. - Context context = ApplicationProvider.getApplicationContext(); - byte[] data = TestUtil.getByteArray(context, file); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ false); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ true); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ true, - /* simulatePartialReads= */ false); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ true, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ true, - /* simulatePartialReads= */ true); - ExtractorAsserts.assertOutput( - new FlacExtractor(), - file, - data, - context, - /* sniffFirst= */ false, - /* simulateIOErrors= */ false, - /* simulateUnknownLength= */ false, - /* simulatePartialReads= */ false); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index a29dfcc310..1f49aee293 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -51,6 +51,12 @@ public final class FragmentedMp4ExtractorTest { ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4"); } + @Test + public void testSampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(Collections.emptyList()), "mp4/sample_ac4_fragmented.mp4"); + } + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index b5c3b26a23..6ddc74c797 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -42,4 +42,9 @@ public final class Mp4ExtractorTest { public void testMp4SampleWithMdatTooLong() throws Exception { ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4"); } + + @Test + public void testMp4SampleWithAc4Track() throws Exception { + ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4"); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index f1b962a712..93e3f30d0c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.common.truth.Truth.assertThat; import android.util.SparseArray; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -172,6 +173,7 @@ public final class TsExtractorTest { } } + @Nullable @Override public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { if (provideCustomEsReader && streamType == 3) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index c617b672e2..7f9549ea75 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -28,4 +28,9 @@ public final class WavExtractorTest { public void testSample() throws Exception { ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); } + + @Test + public void testSampleImaAdpcm() throws Exception { + ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav"); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java index d2bb0fcc5b..34ed88d2d0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/AsynchronousMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import android.media.MediaCodec; import android.media.MediaFormat; @@ -29,7 +29,7 @@ import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -45,27 +45,32 @@ public class AsynchronousMediaCodecAdapterTest { private MediaCodec.BufferInfo bufferInfo; @Before - public void setup() throws IOException { + public void setUp() throws IOException { handlerThread = new HandlerThread("TestHandlerThread"); handlerThread.start(); looper = handlerThread.getLooper(); codec = MediaCodec.createByCodecName("h264"); adapter = new AsynchronousMediaCodecAdapter(codec, looper); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { + adapter.shutdown(); handlerThread.quit(); } @Test public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { + adapter.start(); + assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { + adapter.start(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); @@ -73,6 +78,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); adapter.flush(); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1); @@ -83,9 +89,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush() completes to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); + adapter.start(); Handler handler = new Handler(looper); handler.post( () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); @@ -100,28 +104,35 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (calls.incrementAndGet() == 2) { + throw new IllegalStateException(); + } }); + adapter.start(); adapter.flush(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows( + IllegalStateException.class, + () -> { + adapter.dequeueInputBufferIndex(); + }); } @Test public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { + adapter.start(); + assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); } @Test public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { + adapter.start(); MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); outBufferInfo.presentationTimeUs = 10; adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); @@ -132,6 +143,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { + adapter.start(); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); adapter.flush(); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); @@ -143,9 +155,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush() completes to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); + adapter.start(); Handler handler = new Handler(looper); MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); handler.post( @@ -164,31 +174,23 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger calls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (calls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); + adapter.start(); adapter.flush(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutFormat_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { + adapter.start(); MediaFormat[] formats = new MediaFormat[10]; MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); for (int i = 0; i < formats.length; i++) { @@ -212,6 +214,7 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException { + adapter.start(); MediaFormat format = new MediaFormat(); adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); adapter.dequeueOutputBufferIndex(bufferInfo); @@ -223,13 +226,13 @@ public class AsynchronousMediaCodecAdapterTest { @Test public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException { - AtomicBoolean onCodecStartCalled = new AtomicBoolean(false); - Runnable onCodecStart = () -> onCodecStartCalled.set(true); - adapter.setOnCodecStart(onCodecStart); + AtomicInteger onCodecStartCalled = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet()); + adapter.start(); adapter.flush(); adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); - assertThat(onCodecStartCalled.get()).isFalse(); + assertThat(onCodecStartCalled.get()).isEqualTo(1); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java index 2cfb577579..f974144dd6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/DedicatedThreadAsyncMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; @@ -47,16 +47,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { private MediaCodec.BufferInfo bufferInfo = null; @Before - public void setup() throws IOException { + public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -66,42 +68,15 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.shutdown(); } - @Test - public void start_calledTwice_throwsException() { - adapter.start(); - try { - adapter.start(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - @Test public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -110,11 +85,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test @@ -144,9 +116,6 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -169,39 +138,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test public void dequeueOutputBufferIndex_withInternalException_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -210,11 +158,7 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test @@ -275,42 +219,14 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutStart_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { adapter.start(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); } @Test @@ -351,28 +267,10 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { assertThat(adapter.getOutputFormat()).isEqualTo(format); } - @Test - public void flush_withoutStarted_throwsException() { - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void flush_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - @Test public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -384,23 +282,23 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.flush(); // Enqueues a second flush event handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - // Progress the looper until the milestoneCount is increased - first flush event - // should have been a no-op + // Progress the looper until the milestoneCount is increased. + // adapter.start() will call codec.start(). First flush event should not call codec.start(). ShadowLooper shadowLooper = shadowOf(looper); while (milestoneCount.get() < 1) { shadowLooper.runOneTask(); } - assertThat(onCodecStartCount.get()).isEqualTo(0); + assertThat(codecStartCalls.get()).isEqualTo(1); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(onCodecStartCount.get()).isEqualTo(1); + assertThat(codecStartCalls.get()).isEqualTo(2); } @Test public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet()); adapter.start(); // Obtain looper when adapter is started Looper looper = handlerThread.getLooper(); @@ -408,8 +306,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest { adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); - // only shutdown flushes the MediaCodecAsync handler - assertThat(onCodecStartCount.get()).isEqualTo(0); + // Only adapter.start() calls onCodecStart. + assertThat(onCodecStartCount.get()).isEqualTo(1); } private static class TestHandlerThread extends HandlerThread { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java index b984d28914..c31b86db39 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MultiLockAsyncMediaCodecAdapterTest.java @@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import static org.robolectric.Shadows.shadowOf; import android.media.MediaCodec; @@ -44,20 +44,21 @@ public class MultiLockAsyncMediaCodecAdapterTest { private MultiLockAsyncMediaCodecAdapter adapter; private MediaCodec codec; private MediaCodec.BufferInfo bufferInfo = null; - private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy; private TestHandlerThread handlerThread; @Before - public void setup() throws IOException { + public void setUp() throws IOException { codec = MediaCodec.createByCodecName("h264"); handlerThread = new TestHandlerThread("TestHandlerThread"); adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread); + adapter.setCodecStartRunnable(() -> {}); bufferInfo = new MediaCodec.BufferInfo(); } @After public void tearDown() { adapter.shutdown(); + assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); } @@ -67,42 +68,15 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.shutdown(); } - @Test - public void start_calledTwice_throwsException() { - adapter.start(); - try { - adapter.start(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueInputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } - } - @Test public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new IllegalStateException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new IllegalStateException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -111,11 +85,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } @Test @@ -145,9 +115,6 @@ public class MultiLockAsyncMediaCodecAdapterTest { @Test public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() throws InterruptedException { - // Disable calling codec.start() after flush to avoid receiving buffers from the - // shadow codec impl - adapter.setOnCodecStart(() -> {}); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -170,39 +137,19 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueInputBufferIndex(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex()); } - @Test - public void dequeueOutputBufferIndex_withoutStart_throwsException() { - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void dequeueOutputBufferIndex_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } @Test public void dequeueOutputBufferIndex_withInternalException_throwsException() throws InterruptedException { - adapter.setOnCodecStart( + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable( () -> { - throw new RuntimeException("codec#start() exception"); + if (codecStartCalls.incrementAndGet() == 2) { + throw new RuntimeException("codec#start() exception"); + } }); adapter.start(); adapter.flush(); @@ -211,11 +158,7 @@ public class MultiLockAsyncMediaCodecAdapterTest { waitUntilAllEventsAreExecuted( handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) .isTrue(); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test @@ -276,42 +219,14 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.start(); adapter.onMediaCodecError(new IllegalStateException("error from codec")); - try { - adapter.dequeueOutputBufferIndex(bufferInfo); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_withoutStart_throwsException() { - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void getOutputFormat_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo)); } @Test public void getOutputFormat_withoutFormatReceived_throwsException() { adapter.start(); - try { - adapter.getOutputFormat(); - fail(); - } catch (IllegalStateException expected) { - } + assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat()); } @Test @@ -352,28 +267,10 @@ public class MultiLockAsyncMediaCodecAdapterTest { assertThat(adapter.getOutputFormat()).isEqualTo(format); } - @Test - public void flush_withoutStarted_throwsException() { - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - - @Test - public void flush_afterShutdown_throwsException() { - adapter.start(); - adapter.shutdown(); - try { - adapter.flush(); - } catch (IllegalStateException expected) { - } - } - @Test public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); Looper looper = handlerThread.getLooper(); Handler handler = new Handler(looper); @@ -385,23 +282,23 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.flush(); // Enqueues a second flush event handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); - // Progress the looper until the milestoneCount is increased - first flush event - // should have been a no-op + // Progress the looper until the milestoneCount is increased: + // adapter.start() called codec.start() but first flush event should have been a no-op ShadowLooper shadowLooper = shadowOf(looper); while (milestoneCount.get() < 1) { shadowLooper.runOneTask(); } - assertThat(onCodecStartCount.get()).isEqualTo(0); + assertThat(codecStartCalls.get()).isEqualTo(1); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); - assertThat(onCodecStartCount.get()).isEqualTo(1); + assertThat(codecStartCalls.get()).isEqualTo(2); } @Test public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { - AtomicInteger onCodecStartCount = new AtomicInteger(0); - adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); + AtomicInteger codecStartCalls = new AtomicInteger(0); + adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet()); adapter.start(); // Obtain looper when adapter is started. Looper looper = handlerThread.getLooper(); @@ -409,8 +306,8 @@ public class MultiLockAsyncMediaCodecAdapterTest { adapter.shutdown(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); - // Only shutdown flushes the MediaCodecAsync handler. - assertThat(onCodecStartCount.get()).isEqualTo(0); + // Only adapter.start() called codec#start() + assertThat(codecStartCalls.get()).isEqualTo(1); } private static class TestHandlerThread extends HandlerThread { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java new file mode 100644 index 0000000000..3a71925255 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/SpanUtilTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link SpanUtil}. */ +@RunWith(AndroidJUnit4.class) +public class SpanUtilTest { + + @Test + public void addOrReplaceSpan_replacesSameTypeAndIndexes() { + Spannable spannable = SpannableString.valueOf("test text"); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + /* start= */ 2, + /* end= */ 5, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(newSpan); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentType() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder(); + } + + @Test + public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() { + Spannable spannable = SpannableString.valueOf("test text"); + ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN); + spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE); + SpanUtil.addOrReplaceSpan( + spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN); + SpanUtil.addOrReplaceSpan( + spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + Object[] spans = spannable.getSpans(0, spannable.length(), Object.class); + assertThat(spans) + .asList() + .containsExactly(originalSpan, differentStart, differentEnd, differentFlags) + .inOrder(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 9112bec398..65536f277e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -70,10 +70,10 @@ public final class SsaDecoderTest { assertWithMessage("Cue.positionAnchor") .that(firstCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.lineAnchor").that(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.95f); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -158,33 +158,33 @@ public final class SsaDecoderTest { // Check \pos() sets position & line Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.25f); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.25f); // Check the \pos() doesn't need to be at the start of the line. Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.25f); - assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.25f); + assertThat(secondCue.position).isEqualTo(0.25f); + assertThat(secondCue.line).isEqualTo(0.25f); // Check only the last \pos() value is used. Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); - assertWithMessage("Cue.position").that(thirdCue.position).isEqualTo(0.25f); + assertThat(thirdCue.position).isEqualTo(0.25f); // Check \move() is treated as \pos() Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); - assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.25f); + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.line).isEqualTo(0.25f); // Check alignment override in a separate brace (to bottom-center) affects textAlignment and // both line & position anchors. Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); - assertWithMessage("Cue.position").that(fifthCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.line").that(fifthCue.line).isEqualTo(0.5f); + assertThat(fifthCue.position).isEqualTo(0.5f); + assertThat(fifthCue.line).isEqualTo(0.5f); assertWithMessage("Cue.positionAnchor") .that(fifthCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); - assertWithMessage("Cue.lineAnchor").that(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(fifthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); assertWithMessage("Cue.textAlignment") .that(fifthCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_CENTER); @@ -192,12 +192,12 @@ public final class SsaDecoderTest { // Check alignment override in the same brace (to top-right) affects textAlignment and both line // & position anchors. Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); - assertWithMessage("Cue.position").that(sixthCue.position).isEqualTo(0.5f); - assertWithMessage("Cue.line").that(sixthCue.line).isEqualTo(0.5f); + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.line).isEqualTo(0.5f); assertWithMessage("Cue.positionAnchor") .that(sixthCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_END); - assertWithMessage("Cue.lineAnchor").that(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(sixthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); assertWithMessage("Cue.textAlignment") .that(sixthCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); @@ -212,31 +212,31 @@ public final class SsaDecoderTest { // Negative parameter to \pos() - fall back to the positions implied by middle-left alignment. Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(0.05f); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(0.5f); + assertThat(firstCue.position).isEqualTo(0.05f); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.5f); // Negative parameter to \move() - fall back to the positions implied by middle-left alignment. Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); - assertWithMessage("Cue.position").that(secondCue.position).isEqualTo(0.05f); - assertWithMessage("Cue.lineType").that(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(secondCue.line).isEqualTo(0.5f); + assertThat(secondCue.position).isEqualTo(0.05f); + assertThat(secondCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(secondCue.line).isEqualTo(0.5f); // Check invalid alignment override (11) is skipped and style-provided one is used (4). Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); assertWithMessage("Cue.positionAnchor") .that(thirdCue.positionAnchor) .isEqualTo(Cue.ANCHOR_TYPE_START); - assertWithMessage("Cue.lineAnchor").that(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); assertWithMessage("Cue.textAlignment") .that(thirdCue.textAlignment) .isEqualTo(Layout.Alignment.ALIGN_NORMAL); // No braces - fall back to the positions implied by middle-left alignment Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); - assertWithMessage("Cue.position").that(fourthCue.position).isEqualTo(0.05f); - assertWithMessage("Cue.lineType").that(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(fourthCue.line).isEqualTo(0.5f); + assertThat(fourthCue.position).isEqualTo(0.05f); + assertThat(fourthCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(fourthCue.line).isEqualTo(0.5f); } @Test @@ -250,9 +250,9 @@ public final class SsaDecoderTest { // The dialogue line has a valid \pos() override, but it's ignored because PlayResY isn't // set (so we don't know the denominator). Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); - assertWithMessage("Cue.position").that(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); - assertWithMessage("Cue.lineType").that(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); - assertWithMessage("Cue.line").that(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.position).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index d23ed00e95..aa83fbc8ed 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.text.Spanned; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.RubySpan; import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,13 +35,9 @@ public final class WebvttCueParserTest { + "This is text with html tags"); assertThat(text.toString()).isEqualTo("This is text with html tags"); + assertThat(text).hasUnderlineSpanBetween("This ".length(), "This is".length()); assertThat(text) - .hasUnderlineSpan("This ".length(), "This is".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertThat(text) - .hasBoldItalicSpan( - "This is text with ".length(), - "This is text with html".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasBoldItalicSpanBetween("This is text with ".length(), "This is text with html".length()); } @Test @@ -52,6 +49,36 @@ public final class WebvttCueParserTest { assertThat(text).hasNoSpans(); } + @Test + public void testParseRubyTag() throws Exception { + Spanned text = + parseCueText("Some base textwith ruby and undecorated text"); + + // The text between the tags is stripped from Cue.text and only present on the RubySpan. + assertThat(text.toString()).isEqualTo("Some base text and undecorated text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text".length()) + .withTextAndPosition("with ruby", RubySpan.POSITION_OVER); + } + + @Test + public void testParseRubyTagWithNoTextTag() throws Exception { + Spanned text = parseCueText("Some base text with no ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with no ruby text"); + assertThat(text).hasNoSpans(); + } + + @Test + public void testParseRubyTagWithEmptyTextTag() throws Exception { + Spanned text = parseCueText("Some base text with empty ruby text"); + + assertThat(text.toString()).isEqualTo("Some base text with empty ruby text"); + assertThat(text) + .hasRubySpanBetween("Some ".length(), "Some base text with".length()) + .withTextAndPosition("", RubySpan.POSITION_OVER); + } + @Test public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception { Spanned text = parseCueText("An unclosed u tag with " @@ -59,15 +86,10 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); assertThat(text) - .hasUnderlineSpan( - "An ".length(), - "An unclosed u tag with italic inside".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasUnderlineSpanBetween("An ".length(), "An unclosed u tag with italic inside".length()); assertThat(text) - .hasItalicSpan( - "An unclosed u tag with ".length(), - "An unclosed u tag with italic".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .hasItalicSpanBetween( + "An unclosed u tag with ".length(), "An unclosed u tag with italic".length()); } @Test @@ -76,15 +98,13 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("An italic tag with unclosed underline inside"); assertThat(text) - .hasItalicSpan( + .hasItalicSpanBetween( "An italic tag with unclosed ".length(), - "An italic tag with unclosed underline".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + "An italic tag with unclosed underline".length()); assertThat(text) - .hasUnderlineSpan( + .hasUnderlineSpanBetween( "An italic tag with unclosed ".length(), - "An italic tag with unclosed underline".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + "An italic tag with unclosed underline".length()); } @Test @@ -93,16 +113,11 @@ public final class WebvttCueParserTest { String expectedText = "Overlapping u and i tags"; assertThat(text.toString()).isEqualTo(expectedText); - assertThat(text).hasBoldSpan(0, expectedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween(0, expectedText.length()); // Text between the tags is underlined. - assertThat(text) - .hasUnderlineSpan(0, "Overlapping u and".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasUnderlineSpanBetween(0, "Overlapping u and".length()); // Only text from to <\\u> is italic (unexpected - but simplifies the parsing). - assertThat(text) - .hasItalicSpan( - "Overlapping u ".length(), - "Overlapping u and".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasItalicSpanBetween("Overlapping u ".length(), "Overlapping u and".length()); } @Test @@ -111,8 +126,7 @@ public final class WebvttCueParserTest { assertThat(text.toString()).isEqualTo("foobarbazbuzz"); // endIndex should be 9 when valid (i.e. "foobarbaz".length() - assertThat(text) - .hasBoldSpan("foo".length(), "foobar".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween("foo".length(), "foobar".length()); } @Test @@ -162,13 +176,8 @@ public final class WebvttCueParserTest { Spanned text = parseCueText("blah blah blah foo"); assertThat(text.toString()).isEqualTo("blah blah blah foo"); - assertThat(text) - .hasBoldSpan("blah ".length(), "blah blah".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - assertThat(text) - .hasBoldSpan( - "blah blah blah ".length(), - "blah blah blah foo".length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + assertThat(text).hasBoldSpanBetween("blah ".length(), "blah blah".length()); + assertThat(text).hasBoldSpanBetween("blah blah blah ".length(), "blah blah blah foo".length()); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index f405f1c407..b778953f01 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -15,26 +15,22 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.graphics.Typeface; import android.text.Layout.Alignment; import android.text.Spanned; -import android.text.style.BackgroundColorSpan; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; -import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ColorParser; +import com.google.common.collect.Iterables; import com.google.common.truth.Expect; import java.io.IOException; -import java.util.List; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +49,8 @@ public class WebvttDecoderTest { private static final String WITH_TAGS_FILE = "webvtt/with_tags"; private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors"; + private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT = + "webvtt/with_css_text_combine_upright"; private static final String WITH_BOM = "webvtt/with_bom"; private static final String EMPTY_FILE = "webvtt/empty"; @@ -74,383 +72,307 @@ public class WebvttDecoderTest { public void testDecodeTypical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeWithBom() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BOM); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithBadTimestamps() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithIds() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeTypicalWithComments() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE); - // test event count assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // test cues - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(0 + 1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(2 + 1)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); } @Test public void testDecodeWithTags() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(8); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the &subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the &subtitle."); } @Test public void testDecodeWithPositioning() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); - // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle.", - Alignment.ALIGN_NORMAL, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.1f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_START, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 6, - /* startTimeUs= */ 6000000, - /* endTimeUs= */ 7000000, - "This is the fourth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ -11.0f, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 8, - /* startTimeUs= */ 7000000, - /* endTimeUs= */ 8000000, - "This is the fifth subtitle.", - Alignment.ALIGN_OPPOSITE, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 1.0f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_END, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - assertCue( - subtitle, - /* eventTimeIndex= */ 10, - /* startTimeUs= */ 10000000, - /* endTimeUs= */ 11000000, - "This is the sixth subtitle.", - Alignment.ALIGN_CENTER, - /* line= */ 0.45f, - /* lineType= */ Cue.LINE_TYPE_FRACTION, - /* lineAnchor= */ Cue.ANCHOR_TYPE_END, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 0.35f, - /* verticalType= */ Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCue.position).isEqualTo(0.1f); + assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL); + assertThat(firstCue.size).isEqualTo(0.35f); + // Unspecified values should use WebVTT defaults + assertThat(firstCue.line).isEqualTo(Cue.DIMEN_UNSET); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); + assertThat(firstCue.verticalType).isEqualTo(Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the second subtitle."); + // Position is invalid so defaults to 0.5 + assertThat(secondCue.position).isEqualTo(0.5f); + assertThat(secondCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("This is the third subtitle."); + assertThat(thirdCue.line).isEqualTo(0.45f); + assertThat(thirdCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(thirdCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(thirdCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(thirdCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(6)).isEqualTo(6_000_000L); + assertThat(subtitle.getEventTime(7)).isEqualTo(7_000_000L); + Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); + assertThat(fourthCue.text.toString()).isEqualTo("This is the fourth subtitle."); + assertThat(fourthCue.line).isEqualTo(-11f); + assertThat(fourthCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); + assertThat(fourthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:middle`: + assertThat(fourthCue.position).isEqualTo(0.5f); + assertThat(fourthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + + assertThat(subtitle.getEventTime(8)).isEqualTo(8_000_000L); + assertThat(subtitle.getEventTime(9)).isEqualTo(9_000_000L); + Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8))); + assertThat(fifthCue.text.toString()).isEqualTo("This is the fifth subtitle."); + assertThat(fifthCue.textAlignment).isEqualTo(Alignment.ALIGN_OPPOSITE); + // Derived from `align:right`: + assertThat(fifthCue.position).isEqualTo(1.0f); + assertThat(fifthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + + assertThat(subtitle.getEventTime(10)).isEqualTo(10_000_000L); + assertThat(subtitle.getEventTime(11)).isEqualTo(11_000_000L); + Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10))); + assertThat(sixthCue.text.toString()).isEqualTo("This is the sixth subtitle."); + assertThat(sixthCue.textAlignment).isEqualTo(Alignment.ALIGN_CENTER); + // Derived from `align:center`: + assertThat(sixthCue.position).isEqualTo(0.5f); + assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); } @Test public void testDecodeWithVertical() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_VERTICAL_FILE); - // Test event count. + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "Vertical right-to-left (e.g. Japanese)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - Cue.VERTICAL_TYPE_RL); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "Vertical left-to-right (e.g. Mongolian)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - Cue.VERTICAL_TYPE_LR); - assertCue( - subtitle, - /* eventTimeIndex= */ 4, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "No vertical setting (i.e. horizontal)", - Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); + + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("Vertical right-to-left (e.g. Japanese)"); + assertThat(firstCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); + + assertThat(subtitle.getEventTime(2)).isEqualTo(2_345_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(3_456_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("Vertical left-to-right (e.g. Mongolian)"); + assertThat(secondCue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_LR); + + assertThat(subtitle.getEventTime(4)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(5)).isEqualTo(5_000_000L); + Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); + assertThat(thirdCue.text.toString()).isEqualTo("No vertical setting (i.e. horizontal)"); + assertThat(thirdCue.verticalType).isEqualTo(Cue.TYPE_UNSET); } @Test public void testDecodeWithBadCueHeader() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE); - // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 4000000, - /* endTimeUs= */ 5000000, - "This is the third subtitle."); + assertThat(subtitle.getEventTime(0)).isEqualTo(0L); + assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L); + Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))); + assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle."); + + assertThat(subtitle.getEventTime(2)).isEqualTo(4_000_000L); + assertThat(subtitle.getEventTime(3)).isEqualTo(5_000_000L); + Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); + assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle."); } @Test public void testWebvttWithCssStyle() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); - // Test event count. - assertThat(subtitle.getEventTimeCount()).isEqualTo(8); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 0); + assertThat(firstCueText.toString()).isEqualTo("This is the first subtitle."); + assertThat(firstCueText) + .hasForegroundColorSpanBetween(0, firstCueText.length()) + .withColor(ColorParser.parseCssColor("papayawhip")); + assertThat(firstCueText) + .hasBackgroundColorSpanBetween(0, firstCueText.length()) + .withColor(ColorParser.parseCssColor("green")); - // Test cues. - assertCue( - subtitle, - /* eventTimeIndex= */ 0, - /* startTimeUs= */ 0, - /* endTimeUs= */ 1234000, - "This is the first subtitle."); - assertCue( - subtitle, - /* eventTimeIndex= */ 2, - /* startTimeUs= */ 2345000, - /* endTimeUs= */ 3456000, - "This is the second subtitle."); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 2_345_000); + assertThat(secondCueText.toString()).isEqualTo("This is the second subtitle."); + assertThat(secondCueText) + .hasForegroundColorSpanBetween(0, secondCueText.length()) + .withColor(ColorParser.parseCssColor("peachpuff")); - Spanned s1 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - Spanned s2 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2345000); - Spanned s3 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 20000000); - Spanned s4 = getUniqueSpanTextAt(subtitle, /* timeUs= */ 25000000); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), ForegroundColorSpan.class)).hasLength(1); - assertThat(s1.getSpans(/* start= */ 0, s1.length(), BackgroundColorSpan.class)).hasLength(1); - assertThat(s2.getSpans(/* start= */ 0, s2.length(), ForegroundColorSpan.class)).hasLength(2); - assertThat(s3.getSpans(/* start= */ 10, s3.length(), UnderlineSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 0, /* end= */ 16, BackgroundColorSpan.class)).hasLength(2); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)).hasLength(1); - assertThat(s4.getSpans(/* start= */ 17, s4.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, 20_000_000); + assertThat(thirdCueText.toString()).isEqualTo("This is a reference by element"); + assertThat(thirdCueText).hasUnderlineSpanBetween("This is a ".length(), thirdCueText.length()); + + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, 25_000_000); + assertThat(fourthCueText.toString()).isEqualTo("You are an idiot\nYou don't have the guts"); + assertThat(fourthCueText) + .hasBackgroundColorSpanBetween(0, "You are an idiot".length()) + .withColor(ColorParser.parseCssColor("lime")); + assertThat(fourthCueText) + .hasBoldSpanBetween("You are an idiot\n".length(), fourthCueText.length()); } @Test public void testWithComplexCssSelectors() throws Exception { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); - Spanned text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); - assertThat(text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)) - .hasLength(1); - assertThat( - text.getSpans(/* start= */ 30, text.length(), ForegroundColorSpan.class)[0] - .getForegroundColor()) - .isEqualTo(0xFFEE82EE); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned firstCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 0); + assertThat(firstCueText).hasUnderlineSpanBetween(0, firstCueText.length()); + assertThat(firstCueText) + .hasForegroundColorSpanBetween( + "This should be underlined and ".length(), firstCueText.length()) + .withColor(ColorParser.parseCssColor("violet")); + assertThat(firstCueText) + .hasTypefaceSpanBetween("This should be underlined and ".length(), firstCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2000000); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned secondCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_000_000); + assertThat(secondCueText) + .hasTypefaceSpanBetween("This ".length(), secondCueText.length()) + .withFamily("courier"); + assertThat(secondCueText) + .hasNoForegroundColorSpanBetween("This ".length(), secondCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2500000); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 5, text.length(), TypefaceSpan.class)[0].getFamily()) - .isEqualTo("courier"); + Spanned thirdCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 2_500_000); + assertThat(thirdCueText).hasBoldSpanBetween("This ".length(), thirdCueText.length()); + assertThat(thirdCueText) + .hasTypefaceSpanBetween("This ".length(), thirdCueText.length()) + .withFamily("courier"); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4000000); - assertThat(text.getSpans(/* start= */ 6, /* end= */ 22, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 30, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.BOLD); + Spanned fourthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 4_000_000); + assertThat(fourthCueText) + .hasNoStyleSpanBetween("This ".length(), "shouldn't be bold.".length()); + assertThat(fourthCueText) + .hasBoldSpanBetween("This shouldn't be bold.\nThis ".length(), fourthCueText.length()); - text = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5000000); - assertThat(text.getSpans(/* start= */ 9, /* end= */ 17, StyleSpan.class)).hasLength(0); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)).hasLength(1); - assertThat(text.getSpans(/* start= */ 19, text.length(), StyleSpan.class)[0].getStyle()) - .isEqualTo(Typeface.ITALIC); + Spanned fifthCueText = getUniqueSpanTextAt(subtitle, /* timeUs= */ 5_000_000); + assertThat(fifthCueText) + .hasNoStyleSpanBetween("This is ".length(), "This is specific".length()); + assertThat(fifthCueText) + .hasItalicSpanBetween("This is specific\n".length(), fifthCueText.length()); + } + + @Test + public void testWebvttWithCssTextCombineUpright() throws Exception { + WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_TEXT_COMBINE_UPRIGHT); + + Spanned firstCueText = getUniqueSpanTextAt(subtitle, 500_000); + assertThat(firstCueText) + .hasHorizontalTextInVerticalContextSpanBetween("Combine ".length(), "Combine all".length()); + + Spanned secondCueText = getUniqueSpanTextAt(subtitle, 3_500_000); + assertThat(secondCueText) + .hasHorizontalTextInVerticalContextSpanBetween( + "Combine ".length(), "Combine 0004".length()); } private WebvttSubtitle getSubtitleForTestAsset(String asset) @@ -461,63 +383,6 @@ public class WebvttDecoderTest { } private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) { - return (Spanned) sub.getCues(timeUs).get(0).text; - } - - private void assertCue( - WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, long endTimeUs, String text) { - assertCue( - subtitle, - eventTimeIndex, - startTimeUs, - endTimeUs, - text, - /* textAlignment= */ Alignment.ALIGN_CENTER, - /* line= */ Cue.DIMEN_UNSET, - /* lineType= */ Cue.LINE_TYPE_NUMBER, - /* lineAnchor= */ Cue.ANCHOR_TYPE_START, - /* position= */ 0.5f, - /* positionAnchor= */ Cue.ANCHOR_TYPE_MIDDLE, - /* size= */ 1.0f, - /* verticalType= */ Cue.TYPE_UNSET); - } - - private void assertCue( - WebvttSubtitle subtitle, - int eventTimeIndex, - long startTimeUs, - long endTimeUs, - String text, - @Nullable Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @Cue.AnchorType int lineAnchor, - float position, - @Cue.AnchorType int positionAnchor, - float size, - @Cue.VerticalType int verticalType) { - expect - .withMessage("startTimeUs") - .that(subtitle.getEventTime(eventTimeIndex)) - .isEqualTo(startTimeUs); - expect - .withMessage("endTimeUs") - .that(subtitle.getEventTime(eventTimeIndex + 1)) - .isEqualTo(endTimeUs); - List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex)); - assertThat(cues).hasSize(1); - // Assert cue properties. - Cue cue = cues.get(0); - expect.withMessage("cue.text").that(cue.text.toString()).isEqualTo(text); - expect.withMessage("cue.textAlignment").that(cue.textAlignment).isEqualTo(textAlignment); - expect.withMessage("cue.line").that(cue.line).isEqualTo(line); - expect.withMessage("cue.lineType").that(cue.lineType).isEqualTo(lineType); - expect.withMessage("cue.lineAnchor").that(cue.lineAnchor).isEqualTo(lineAnchor); - expect.withMessage("cue.position").that(cue.position).isEqualTo(position); - expect.withMessage("cue.positionAnchor").that(cue.positionAnchor).isEqualTo(positionAnchor); - expect.withMessage("cue.size").that(cue.size).isEqualTo(size); - expect.withMessage("cue.verticalType").that(cue.verticalType).isEqualTo(verticalType); - - assertThat(expect.hasFailures()).isFalse(); + return (Spanned) Assertions.checkNotNull(sub.getCues(timeUs).get(0).text); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java deleted file mode 100644 index 8b20630a23..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptiveTrackSelectionTest.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * 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.trackselection; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; - -import android.util.Pair; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -/** Unit test for the track selection created by {@link BufferSizeAdaptationBuilder}. */ -@RunWith(AndroidJUnit4.class) -public final class BufferSizeAdaptiveTrackSelectionTest { - - private static final int MIN_BUFFER_MS = 15_000; - private static final int MAX_BUFFER_MS = 50_000; - private static final int HYSTERESIS_BUFFER_MS = 10_000; - private static final float BANDWIDTH_FRACTION = 0.5f; - private static final int MIN_BUFFER_FOR_QUALITY_INCREASE_MS = 10_000; - - /** - * Factor between bitrates is always the same (=2.2). That means buffer levels should be linearly - * distributed between MIN_BUFFER=15s and MAX_BUFFER-HYSTERESIS=50s-10s=40s. - */ - private static final Format format1 = - createVideoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); - - private static final Format format2 = - createVideoFormat(/* bitrate= */ 1100, /* width= */ 640, /* height= */ 480); - private static final Format format3 = - createVideoFormat(/* bitrate= */ 2420, /* width= */ 960, /* height= */ 720); - private static final int BUFFER_LEVEL_FORMAT_2 = - (MIN_BUFFER_MS + MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS) / 2; - private static final int BUFFER_LEVEL_FORMAT_3 = MAX_BUFFER_MS - HYSTERESIS_BUFFER_MS; - - @Mock private BandwidthMeter mockBandwidthMeter; - private TrackSelection trackSelection; - - @Before - public void setUp() { - initMocks(this); - Pair trackSelectionFactoryAndLoadControl = - new BufferSizeAdaptationBuilder() - .setBufferDurationsMs( - MIN_BUFFER_MS, - MAX_BUFFER_MS, - /* bufferForPlaybackMs= */ 1000, - /* bufferForPlaybackAfterRebufferMs= */ 1000) - .setHysteresisBufferMs(HYSTERESIS_BUFFER_MS) - .setStartUpTrackSelectionParameters( - BANDWIDTH_FRACTION, MIN_BUFFER_FOR_QUALITY_INCREASE_MS) - .buildPlayerComponents(); - trackSelection = - trackSelectionFactoryAndLoadControl - .first - .createTrackSelections( - new TrackSelection.Definition[] { - new TrackSelection.Definition( - new TrackGroup(format1, format2, format3), /* tracks= */ 0, 1, 2) - }, - mockBandwidthMeter)[0]; - trackSelection.enable(); - } - - @Test - public void updateSelectedTrack_usesBandwidthEstimateForInitialSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withLowerBandwidthEstimateDuringStartUp_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ 0); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andLowBuffer_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withHigherBandwidthEstimateDuringStartUp_andHighBuffer_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ MIN_BUFFER_FOR_QUALITY_INCREASE_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withIncreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format3)); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void - updateSelectedTrack_withDecreasedBandwidthEstimate_onceSteadyStateBufferIsReached_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void updateSelectedTrack_withIncreasedBufferInSteadyState_switchesUp() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_3); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format3); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void updateSelectedTrack_withDecreasedBufferInSteadyState_switchesDown() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - @Test - public void - updateSelectedTrack_withDecreasedBufferInSteadyState_withinHysteresis_keepsSelection() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - HYSTERESIS_BUFFER_MS); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format2); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); - } - - @Test - public void onDiscontinuity_switchesBackToStartUpState() { - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(getBitrateEstimateEnoughFor(format2)); - updateSelectedTrack(/* bufferedDurationMs= */ 0); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2); - when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(0L); - - trackSelection.onDiscontinuity(); - updateSelectedTrack(/* bufferedDurationMs= */ BUFFER_LEVEL_FORMAT_2 - 1); - - assertThat(trackSelection.getSelectedFormat()).isEqualTo(format1); - assertThat(trackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); - } - - private void updateSelectedTrack(long bufferedDurationMs) { - trackSelection.updateSelectedTrack( - /* playbackPositionUs= */ 0, - /* bufferedDurationUs= */ C.msToUs(bufferedDurationMs), - /* availableDurationUs= */ C.TIME_UNSET, - /* queue= */ Collections.emptyList(), - /* mediaChunkIterators= */ new MediaChunkIterator[] { - MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY - }); - } - - private static Format createVideoFormat(int bitrate, int width, int height) { - return Format.createVideoSampleFormat( - /* id= */ null, - /* sampleMimeType= */ MimeTypes.VIDEO_H264, - /* codecs= */ null, - /* bitrate= */ bitrate, - /* maxInputSize= */ Format.NO_VALUE, - /* width= */ width, - /* height= */ height, - /* frameRate= */ Format.NO_VALUE, - /* initializationData= */ null, - /* drmInitData= */ null); - } - - private static long getBitrateEstimateEnoughFor(Format format) { - return (long) (format.bitrate / BANDWIDTH_FRACTION) + 1; - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index a8dbfe3b42..4d9a936c4e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -204,7 +204,7 @@ public class SimpleCacheTest { simpleCache.releaseHoleSpan(cacheSpan2); simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first()); - // Don't release the cache. This means the index file wont have been written to disk after the + // Don't release the cache. This means the index file won't have been written to disk after the // data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the // folder locking check. File cacheDir2 = 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 index ddaa550b7f..d1b0363d20 100644 --- 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 @@ -17,9 +17,12 @@ package com.google.android.exoplayer2.util; import static com.google.common.truth.Truth.assertThat; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.VorbisComment; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,6 +31,27 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class FlacStreamMetadataTest { + @Test + public void constructFromByteArray_setsFieldsCorrectly() throws IOException { + byte[] fileData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "flac/bear.flac"); + + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata( + fileData, FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + assertThat(streamMetadata.minBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.maxBlockSizeSamples).isEqualTo(4096); + assertThat(streamMetadata.minFrameSize).isEqualTo(445); + assertThat(streamMetadata.maxFrameSize).isEqualTo(5776); + assertThat(streamMetadata.sampleRate).isEqualTo(48000); + assertThat(streamMetadata.sampleRateLookupKey).isEqualTo(10); + assertThat(streamMetadata.channels).isEqualTo(2); + assertThat(streamMetadata.bitsPerSample).isEqualTo(16); + assertThat(streamMetadata.bitsPerSampleLookupKey).isEqualTo(4); + assertThat(streamMetadata.totalSamples).isEqualTo(131568); + } + @Test public void parseVorbisComments() { ArrayList commentsList = new ArrayList<>(); 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 a6636d71be..bfb4e018f0 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 @@ -49,8 +49,8 @@ import java.util.concurrent.CopyOnWriteArrayList; * A view for controlling {@link Player} instances. * *

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

Attributes

* @@ -104,6 +104,30 @@ import java.util.concurrent.CopyOnWriteArrayList; * layout is overridden to specify a custom {@code exo_progress} (see below). * * + *

Overriding drawables

+ * + * The drawables used by PlayerControlView (with its default layout file) can be overridden by + * drawables with the same names defined in your application. The drawables that can be overridden + * are: + * + *
    + *
  • {@code exo_controls_play} - The play icon. + *
  • {@code exo_controls_pause} - The pause icon. + *
  • {@code exo_controls_rewind} - The rewind icon. + *
  • {@code exo_controls_fastforward} - The fast forward icon. + *
  • {@code exo_controls_previous} - The previous icon. + *
  • {@code exo_controls_next} - The next icon. + *
  • {@code exo_controls_repeat_off} - The repeat icon for {@link + * Player#REPEAT_MODE_OFF}. + *
  • {@code exo_controls_repeat_one} - The repeat icon for {@link + * Player#REPEAT_MODE_ONE}. + *
  • {@code exo_controls_repeat_all} - The repeat icon for {@link + * Player#REPEAT_MODE_ALL}. + *
  • {@code exo_controls_shuffle_off} - The shuffle icon when shuffling is disabled. + *
  • {@code exo_controls_shuffle_on} - The shuffle icon when shuffling is enabled. + *
  • {@code exo_controls_vr} - The VR icon. + *
+ * *

Overriding the layout file

* * To customize the layout of PlayerControlView throughout your app, or just for certain @@ -123,29 +147,38 @@ import java.util.concurrent.CopyOnWriteArrayList; *
    *
  • Type: {@link View} *
- *
  • {@code exo_ffwd} - The fast forward button. - *
      - *
    • Type: {@link View} - *
    *
  • {@code exo_rew} - The rewind button. *
      *
    • Type: {@link View} *
    - *
  • {@code exo_prev} - The previous track button. + *
  • {@code exo_ffwd} - The fast forward button. *
      *
    • Type: {@link View} *
    - *
  • {@code exo_next} - The next track button. + *
  • {@code exo_prev} - The previous button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_next} - The next button. *
      *
    • Type: {@link View} *
    *
  • {@code exo_repeat_toggle} - The repeat toggle button. *
      - *
    • Type: {@link View} + *
    • Type: {@link ImageView} + *
    • Note: PlayerControlView will programmatically set the drawable on the repeat toggle + * button according to the player's current repeat mode. The drawables used are {@code + * exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code + * exo_controls_repeat_all}. See the section above for information on overriding these + * drawables. *
    *
  • {@code exo_shuffle} - The shuffle button. *
      - *
    • Type: {@link View} + *
    • Type: {@link ImageView} + *
    • Note: PlayerControlView will programmatically set the drawable on the shuffle button + * according to the player's current repeat mode. The drawables used are {@code + * exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above + * for information on overriding these drawables. *
    *
  • {@code exo_vr} - The VR mode button. *
      @@ -910,7 +943,7 @@ public class PlayerControlView extends FrameLayout { adGroupTimeInPeriodUs = period.durationUs; } long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupTimeInWindowUs >= 0) { if (adGroupCount == adGroupTimesMs.length) { int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); 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 aeb5292187..9f0c8280c4 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 @@ -55,28 +55,28 @@ import java.util.List; import java.util.Map; /** - * A notification manager to start, update and cancel a media style notification reflecting the - * player state. + * Starts, updates and cancels a media style notification reflecting the player state. The actions + * displayed and the drawables used can both be customized, as described below. * *

      The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or * when the notification is dismissed by the user. * *

      If the player is released it must be removed from the manager by calling {@code - * setPlayer(null)} which will cancel the notification. + * setPlayer(null)}. * *

      Action customization

      * - * Standard playback actions can be shown or omitted as follows: + * Playback actions can be displayed or omitted as follows: * *
        - *
      • {@code useNavigationActions} - Sets whether the navigation previous and next actions - * are displayed. + *
      • {@code useNavigationActions} - Sets whether the previous and next actions are + * displayed. *
          *
        • Corresponding setter: {@link #setUseNavigationActions(boolean)} *
        • Default: {@code true} *
        - *
      • {@code useNavigationActionsInCompactView} - Sets whether the navigation previous and - * next actions should are displayed in compact view (including the lock screen notification). + *
      • {@code useNavigationActionsInCompactView} - Sets whether the previous and next + * actions are displayed in compact view (including the lock screen notification). *
          *
        • Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)} *
        • Default: {@code false} @@ -98,12 +98,35 @@ import java.util.Map; *
        • Default: {@link #DEFAULT_REWIND_MS} (5000) *
        *
      • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the - * fast forward action is not included in the notification. + * fast forward action is not displayed. *
          *
        • Corresponding setter: {@link #setFastForwardIncrementMs(long)} - *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) + *
        • Default: {@link #DEFAULT_FAST_FORWARD_MS} (15000) *
        *
      + * + *

      Overriding drawables

      + * + * The drawables used by PlayerNotificationManager can be overridden by drawables with the same + * names defined in your application. The drawables that can be overridden are: + * + *
        + *
      • {@code exo_notification_small_icon} - The icon passed by default to {@link + * NotificationCompat.Builder#setSmallIcon(int)}. A different icon can also be specified + * programmatically by calling {@link #setSmallIcon(int)}. + *
      • {@code exo_notification_play} - The play icon. + *
      • {@code exo_notification_pause} - The pause icon. + *
      • {@code exo_notification_rewind} - The rewind icon. + *
      • {@code exo_notification_fastforward} - The fast forward icon. + *
      • {@code exo_notification_previous} - The previous icon. + *
      • {@code exo_notification_next} - The next icon. + *
      • {@code exo_notification_stop} - The stop icon. + *
      + * + * Unlike the drawables above, the large icon (i.e. the icon passed to {@link + * NotificationCompat.Builder#setLargeIcon(Bitmap)} cannot be overridden in this way. Instead, the + * large icon is obtained from the {@link MediaDescriptionAdapter} injected when creating the + * PlayerNotificationManager. */ public class PlayerNotificationManager { @@ -154,11 +177,10 @@ public class PlayerNotificationManager { /** * Gets the large icon for the current media item. * - *

      When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be - * returned and the bitmap asynchronously passed to the {@link BitmapCallback} once it is - * loaded. Because the adapter may be called multiple times for the same media item, the bitmap - * should be cached by the app and whenever possible be returned synchronously at subsequent - * calls for the same media item. + *

      When a bitmap needs to be loaded asynchronously, a placeholder bitmap (or null) should be + * returned. The actual bitmap should be passed to the {@link BitmapCallback} once it has been + * loaded. Because the adapter may be called multiple times for the same media item, bitmaps + * should be cached by the app and returned synchronously when possible. * *

      See {@link NotificationCompat.Builder#setLargeIcon(Bitmap)}. * @@ -905,7 +927,18 @@ public class PlayerNotificationManager { } /** - * Sets whether the elapsed time of the media playback should be displayed + * Sets whether the elapsed time of the media playback should be displayed. + * + *

      Note that this setting only works if all of the following are true: + * + *

        + *
      • The media is {@link Player#isPlaying() actively playing}. + *
      • The media is not {@link Player#isCurrentWindowDynamic() dynamically changing its + * duration} (like for example a live stream). + *
      • The media is not {@link Player#isPlayingAd() interrupted by an ad}. + *
      • The media is played at {@link Player#getPlaybackParameters() regular speed}. + *
      • The device is running at least API 21 (Lollipop). + *
      * *

      See {@link NotificationCompat.Builder#setUsesChronometer(boolean)}. * @@ -1060,7 +1093,8 @@ public class PlayerNotificationManager { && useChronometer && player.isPlaying() && !player.isPlayingAd() - && !player.isCurrentWindowDynamic()) { + && !player.isCurrentWindowDynamic() + && player.getPlaybackParameters().speed == 1f) { builder .setWhen(System.currentTimeMillis() - player.getContentPosition()) .setShowWhen(true) 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 c55fe09f76..03168643cf 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 @@ -79,7 +79,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * during playback, and displays playback controls using a {@link PlayerControlView}. * *

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

      Attributes

      * @@ -172,6 +173,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * exo_controller} (see below). *
    * + *

    Overriding drawables

    + * + * The drawables used by {@link PlayerControlView} (with its default layout file) can be overridden + * by drawables with the same names defined in your application. See the {@link PlayerControlView} + * documentation for a list of drawables that can be overridden. + * *

    Overriding the layout file

    * * To customize the layout of PlayerView throughout your app, or just for certain configurations, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java index 0015634c1f..1751502ac4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/truth/SpannedSubject.java @@ -23,12 +23,22 @@ import static com.google.common.truth.Truth.assertAbout; import android.graphics.Typeface; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.CheckResult; +import androidx.annotation.ColorInt; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; +import com.google.android.exoplayer2.util.Util; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; /** A Truth {@link Subject} for assertions on {@link Spanned} instances containing text styling. */ @@ -64,57 +74,59 @@ public final class SpannedSubject extends Subject { failWithoutActual( simpleFact("Expected no spans"), fact("in text", actual), - fact("but found", actualSpansString())); + fact("but found", getAllSpansAsString(actual))); } } /** - * Checks that the subject has an italic span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has an italic span from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - public void hasItalicSpan(int startIndex, int endIndex, int flags) { - hasStyleSpan(startIndex, endIndex, flags, Typeface.ITALIC); + public WithSpanFlags hasItalicSpanBetween(int start, int end) { + return hasStyleSpan(start, end, Typeface.ITALIC); } /** - * Checks that the subject has a bold span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has a bold span from {@code start} to {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - public void hasBoldSpan(int startIndex, int endIndex, int flags) { - hasStyleSpan(startIndex, endIndex, flags, Typeface.BOLD); + public WithSpanFlags hasBoldSpanBetween(int start, int end) { + return hasStyleSpan(start, end, Typeface.BOLD); } - private void hasStyleSpan(int startIndex, int endIndex, int flags, int style) { + private WithSpanFlags hasStyleSpan(int start, int end, int style) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } - for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) { + List allFlags = new ArrayList<>(); + boolean matchingSpanFound = false; + for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { + allFlags.add(actual.getSpanFlags(span)); if (span.getStyle() == style) { - return; + matchingSpanFound = true; + break; } } + if (matchingSpanFound) { + return check("StyleSpan (start=%s,end=%s,style=%s)", start, end, style) + .about(spanFlags()) + .that(allFlags); + } - failWithExpectedSpan( - startIndex, - endIndex, - flags, - new StyleSpan(style), - actual.toString().substring(startIndex, endIndex)); + failWithExpectedSpan(start, end, StyleSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; } /** - * Checks that the subject has bold and italic styling from {@code startIndex} to {@code - * endIndex}. + * Checks that the subject has bold and italic styling from {@code start} to {@code end}. * *

    This can either be: * @@ -124,80 +136,322 @@ public final class SpannedSubject extends Subject { * with {@code span.getStyle() == Typeface.ITALIC}. * * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. */ - public void hasBoldItalicSpan(int startIndex, int endIndex, int flags) { + public WithSpanFlags hasBoldItalicSpanBetween(int start, int end) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); - return; + return ALREADY_FAILED_WITH_FLAGS; } + List allFlags = new ArrayList<>(); List styles = new ArrayList<>(); - for (StyleSpan span : findMatchingSpans(startIndex, endIndex, flags, StyleSpan.class)) { + for (StyleSpan span : findMatchingSpans(start, end, StyleSpan.class)) { + allFlags.add(actual.getSpanFlags(span)); styles.add(span.getStyle()); } - if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC)) { - return; - } else if (styles.size() == 2 - && styles.contains(Typeface.BOLD) - && styles.contains(Typeface.ITALIC)) { - return; + if (styles.isEmpty()) { + failWithExpectedSpan(start, end, StyleSpan.class, actual.subSequence(start, end).toString()); + return ALREADY_FAILED_WITH_FLAGS; } - String spannedSubstring = actual.toString().substring(startIndex, endIndex); - String boldSpan = - spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD), spannedSubstring); - String italicSpan = - spanToString(startIndex, endIndex, flags, new StyleSpan(Typeface.ITALIC), spannedSubstring); - String boldItalicSpan = - spanToString( - startIndex, endIndex, flags, new StyleSpan(Typeface.BOLD_ITALIC), spannedSubstring); - + if (styles.size() == 1 && styles.contains(Typeface.BOLD_ITALIC) + || styles.size() == 2 + && styles.contains(Typeface.BOLD) + && styles.contains(Typeface.ITALIC)) { + return check("StyleSpan (start=%s,end=%s)", start, end).about(spanFlags()).that(allFlags); + } failWithoutActual( - simpleFact("No matching span found"), + simpleFact( + String.format("No matching StyleSpans found between start=%s,end=%s", start, end)), fact("in text", actual.toString()), - fact("expected either", boldItalicSpan), - fact("or both", boldSpan + "\n" + italicSpan), - fact("but found", actualSpansString())); + fact("expected either styles", Collections.singletonList(Typeface.BOLD_ITALIC)), + fact("or styles", Arrays.asList(Typeface.BOLD, Typeface.ITALIC)), + fact("but found styles", styles)); + return ALREADY_FAILED_WITH_FLAGS; } /** - * Checks that the subject has an underline span from {@code startIndex} to {@code endIndex}. + * Checks that the subject has no {@link StyleSpan}s on any of the text between {@code start} and + * {@code end}. * - * @param startIndex The start of the expected span. - * @param endIndex The end of the expected span. - * @param flags The flags of the expected span. See constants on {@link Spanned} for more - * information. + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. */ - public void hasUnderlineSpan(int startIndex, int endIndex, int flags) { + public void hasNoStyleSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(StyleSpan.class, start, end); + } + + /** + * Checks that the subject has an {@link UnderlineSpan} from {@code start} to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public WithSpanFlags hasUnderlineSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_FLAGS; + } + + List underlineSpans = findMatchingSpans(start, end, UnderlineSpan.class); + if (underlineSpans.size() == 1) { + return check("UnderlineSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(underlineSpans.get(0)))); + } + failWithExpectedSpan(start, end, UnderlineSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + + /** + * Checks that the subject has no {@link UnderlineSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoUnderlineSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(UnderlineSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link ForegroundColorSpan} from {@code start} to {@code end}. + * + *

    The color is asserted in a follow-up method call on the return {@link Colored} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public Colored hasForegroundColorSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_COLORED; + } + + List foregroundColorSpans = + findMatchingSpans(start, end, ForegroundColorSpan.class); + if (foregroundColorSpans.isEmpty()) { + failWithExpectedSpan( + start, end, ForegroundColorSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_COLORED; + } + return check("ForegroundColorSpan (start=%s,end=%s)", start, end) + .about(foregroundColorSpans(actual)) + .that(foregroundColorSpans); + } + + /** + * Checks that the subject has no {@link ForegroundColorSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoForegroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(ForegroundColorSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link BackgroundColorSpan} from {@code start} to {@code end}. + * + *

    The color is asserted in a follow-up method call on the return {@link Colored} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public Colored hasBackgroundColorSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_COLORED; + } + + List backgroundColorSpans = + findMatchingSpans(start, end, BackgroundColorSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpan( + start, end, BackgroundColorSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_COLORED; + } + return check("BackgroundColorSpan (start=%s,end=%s)", start, end) + .about(backgroundColorSpans(actual)) + .that(backgroundColorSpans); + } + + /** + * Checks that the subject has no {@link BackgroundColorSpan}s on any of the text between {@code + * start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoBackgroundColorSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(BackgroundColorSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link TypefaceSpan} from {@code start} to {@code end}. + * + *

    The font is asserted in a follow-up method call on the return {@link Typefaced} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Typefaced} object to assert on the font of the matching spans. + */ + @CheckResult + public Typefaced hasTypefaceSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_TYPEFACED; + } + + List backgroundColorSpans = findMatchingSpans(start, end, TypefaceSpan.class); + if (backgroundColorSpans.isEmpty()) { + failWithExpectedSpan(start, end, TypefaceSpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_TYPEFACED; + } + return check("TypefaceSpan (start=%s,end=%s)", start, end) + .about(typefaceSpans(actual)) + .that(backgroundColorSpans); + } + + /** + * Checks that the subject has no {@link TypefaceSpan}s on any of the text between {@code start} + * and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoTypefaceSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(TypefaceSpan.class, start, end); + } + + /** + * Checks that the subject has a {@link RubySpan} from {@code start} to {@code end}. + * + *

    The ruby-text is asserted in a follow-up method call on the return {@link RubyText} object. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link Colored} object to assert on the color of the matching spans. + */ + @CheckResult + public RubyText hasRubySpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_TEXT; + } + + List rubySpans = findMatchingSpans(start, end, RubySpan.class); + if (rubySpans.isEmpty()) { + failWithExpectedSpan(start, end, RubySpan.class, actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_TEXT; + } + return check("RubySpan (start=%s,end=%s)", start, end).about(rubySpans(actual)).that(rubySpans); + } + + /** + * Checks that the subject has no {@link RubySpan}s on any of the text between {@code start} and + * {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoRubySpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(RubySpan.class, start, end); + } + + /** + * Checks that the subject has an {@link HorizontalTextInVerticalContextSpan} from {@code start} + * to {@code end}. + * + * @param start The start of the expected span. + * @param end The end of the expected span. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + public WithSpanFlags hasHorizontalTextInVerticalContextSpanBetween(int start, int end) { + if (actual == null) { + failWithoutActual(simpleFact("Spanned must not be null")); + return ALREADY_FAILED_WITH_FLAGS; + } + + List horizontalInVerticalSpans = + findMatchingSpans(start, end, HorizontalTextInVerticalContextSpan.class); + if (horizontalInVerticalSpans.size() == 1) { + return check("HorizontalTextInVerticalContextSpan (start=%s,end=%s)", start, end) + .about(spanFlags()) + .that(Collections.singletonList(actual.getSpanFlags(horizontalInVerticalSpans.get(0)))); + } + failWithExpectedSpan( + start, + end, + HorizontalTextInVerticalContextSpan.class, + actual.toString().substring(start, end)); + return ALREADY_FAILED_WITH_FLAGS; + } + + /** + * Checks that the subject has no {@link HorizontalTextInVerticalContextSpan}s on any of the text + * between {@code start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + public void hasNoHorizontalTextInVerticalContextSpanBetween(int start, int end) { + hasNoSpansOfTypeBetween(HorizontalTextInVerticalContextSpan.class, start, end); + } + + /** + * Checks that the subject has no spans of type {@code spanClazz} on any of the text between + * {@code start} and {@code end}. + * + *

    This fails even if the start and end indexes don't exactly match. + * + * @param start The start index to start searching for spans. + * @param end The end index to stop searching for spans. + */ + private void hasNoSpansOfTypeBetween(Class spanClazz, int start, int end) { if (actual == null) { failWithoutActual(simpleFact("Spanned must not be null")); return; } - - List underlineSpans = - findMatchingSpans(startIndex, endIndex, flags, UnderlineSpan.class); - if (underlineSpans.size() == 1) { - return; + Object[] matchingSpans = actual.getSpans(start, end, spanClazz); + if (matchingSpans.length != 0) { + failWithoutActual( + simpleFact( + String.format( + "Found unexpected %ss between start=%s,end=%s", + spanClazz.getSimpleName(), start, end)), + simpleFact("expected none"), + fact("but found", getAllSpansAsString(actual))); } - failWithExpectedSpan( - startIndex, - endIndex, - flags, - new UnderlineSpan(), - actual.toString().substring(startIndex, endIndex)); } - private List findMatchingSpans( - int startIndex, int endIndex, int flags, Class spanClazz) { + private List findMatchingSpans(int startIndex, int endIndex, Class spanClazz) { List spans = new ArrayList<>(); for (T span : actual.getSpans(startIndex, endIndex, spanClazz)) { - if (actual.getSpanStart(span) == startIndex - && actual.getSpanEnd(span) == endIndex - && actual.getSpanFlags(span) == flags) { + if (actual.getSpanStart(span) == startIndex && actual.getSpanEnd(span) == endIndex) { spans.add(span); } } @@ -205,43 +459,321 @@ public final class SpannedSubject extends Subject { } private void failWithExpectedSpan( - int start, int end, int flags, Object span, String spannedSubstring) { + int start, int end, Class spanType, String spannedSubstring) { failWithoutActual( simpleFact("No matching span found"), fact("in text", actual), - fact("expected", spanToString(start, end, flags, span, spannedSubstring)), - fact("but found", actualSpansString())); + fact("expected", getSpanAsString(start, end, spanType, spannedSubstring)), + fact("but found", getAllSpansAsString(actual))); } - private String actualSpansString() { + private static String getAllSpansAsString(Spanned spanned) { List actualSpanStrings = new ArrayList<>(); - for (Object span : actual.getSpans(0, actual.length(), /* type= */ Object.class)) { - actualSpanStrings.add(spanToString(span, actual)); + for (Object span : spanned.getSpans(0, spanned.length(), Object.class)) { + actualSpanStrings.add(getSpanAsString(span, spanned)); } return TextUtils.join("\n", actualSpanStrings); } - private static String spanToString(Object span, Spanned spanned) { + private static String getSpanAsString(Object span, Spanned spanned) { int spanStart = spanned.getSpanStart(span); int spanEnd = spanned.getSpanEnd(span); - return spanToString( - spanStart, - spanEnd, - spanned.getSpanFlags(span), - span, - spanned.toString().substring(spanStart, spanEnd)); + return getSpanAsString( + spanStart, spanEnd, span.getClass(), spanned.toString().substring(spanStart, spanEnd)); } - private static String spanToString( - int start, int end, int flags, Object span, String spannedSubstring) { - String suffix; - if (span instanceof StyleSpan) { - suffix = "\tstyle=" + ((StyleSpan) span).getStyle(); - } else { - suffix = ""; - } + private static String getSpanAsString( + int start, int end, Class span, String spannedSubstring) { return String.format( - "start=%s\tend=%s\tflags=%s\ttype=%s\tsubstring='%s'%s", - start, end, flags, span.getClass().getSimpleName(), spannedSubstring, suffix); + "start=%s\tend=%s\ttype=%s\tsubstring='%s'", + start, end, span.getSimpleName(), spannedSubstring); + } + + /** + * Allows additional assertions to be made on the flags of matching spans. + * + *

    Identical to {@link WithSpanFlags}, but this should be returned from {@code with...()} + * methods while {@link WithSpanFlags} should be returned from {@code has...()} methods. + * + *

    See Flag constants on {@link Spanned} for possible values. + */ + public interface AndSpanFlags { + + /** + * Checks that one of the matched spans has the expected {@code flags}. + * + * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. + */ + void andFlags(int flags); + } + + private static final AndSpanFlags ALREADY_FAILED_AND_FLAGS = flags -> {}; + + /** + * Allows additional assertions to be made on the flags of matching spans. + * + *

    Identical to {@link AndSpanFlags}, but this should be returned from {@code has...()} methods + * while {@link AndSpanFlags} should be returned from {@code with...()} methods. + */ + public interface WithSpanFlags { + + /** + * Checks that one of the matched spans has the expected {@code flags}. + * + * @param flags The expected flags. See SPAN_* constants on {@link Spanned} for possible values. + */ + void withFlags(int flags); + } + + private static final WithSpanFlags ALREADY_FAILED_WITH_FLAGS = flags -> {}; + + private static Factory> spanFlags() { + return SpanFlagsSubject::new; + } + + private static final class SpanFlagsSubject extends Subject + implements AndSpanFlags, WithSpanFlags { + + private final List flags; + + private SpanFlagsSubject(FailureMetadata metadata, List flags) { + super(metadata, flags); + this.flags = flags; + } + + @Override + public void andFlags(int flags) { + check("contains()").that(this.flags).contains(flags); + } + + @Override + public void withFlags(int flags) { + andFlags(flags); + } + } + + /** Allows assertions about the color of a span. */ + public interface Colored { + + /** + * Checks that at least one of the matched spans has the expected {@code color}. + * + * @param color The expected color. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withColor(@ColorInt int color); + } + + private static final Colored ALREADY_FAILED_COLORED = color -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> + foregroundColorSpans(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new ForegroundColorSpansSubject(metadata, spans, actualSpanned); + } + + private static final class ForegroundColorSpansSubject extends Subject implements Colored { + + private final List actualSpans; + private final Spanned actualSpanned; + + private ForegroundColorSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withColor(@ColorInt int color) { + List matchingSpanFlags = new ArrayList<>(); + // Use hex strings for comparison so the values in error messages are more human readable. + List spanColors = new ArrayList<>(); + + for (ForegroundColorSpan span : actualSpans) { + spanColors.add(String.format("0x%08X", span.getForegroundColor())); + if (span.getForegroundColor() == color) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + String expectedColorString = String.format("0x%08X", color); + check("foregroundColor").that(spanColors).containsExactly(expectedColorString); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + + private static Factory> + backgroundColorSpans(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new BackgroundColorSpansSubject(metadata, spans, actualSpanned); + } + + private static final class BackgroundColorSpansSubject extends Subject implements Colored { + + private final List actualSpans; + private final Spanned actualSpanned; + + private BackgroundColorSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withColor(@ColorInt int color) { + List matchingSpanFlags = new ArrayList<>(); + // Use hex strings for comparison so the values in error messages are more human readable. + List spanColors = new ArrayList<>(); + + for (BackgroundColorSpan span : actualSpans) { + spanColors.add(String.format("0x%08X", span.getBackgroundColor())); + if (span.getBackgroundColor() == color) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + String expectedColorString = String.format("0x%08X", color); + check("backgroundColor").that(spanColors).containsExactly(expectedColorString); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + + /** Allows assertions about the typeface of a span. */ + public interface Typefaced { + + /** + * Checks that at least one of the matched spans has the expected {@code fontFamily}. + * + * @param fontFamily The expected font family. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withFamily(String fontFamily); + } + + private static final Typefaced ALREADY_FAILED_TYPEFACED = color -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> typefaceSpans( + Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new TypefaceSpansSubject(metadata, spans, actualSpanned); + } + + private static final class TypefaceSpansSubject extends Subject implements Typefaced { + + private final List actualSpans; + private final Spanned actualSpanned; + + private TypefaceSpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withFamily(String fontFamily) { + List matchingSpanFlags = new ArrayList<>(); + List spanFontFamilies = new ArrayList<>(); + + for (TypefaceSpan span : actualSpans) { + spanFontFamilies.add(span.getFamily()); + if (Util.areEqual(span.getFamily(), fontFamily)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + + check("family").that(spanFontFamilies).containsExactly(fontFamily); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + } + + /** Allows assertions about a span's ruby text and its position. */ + public interface RubyText { + + /** + * Checks that at least one of the matched spans has the expected {@code text}. + * + * @param text The expected text. + * @param position The expected position of the text. + * @return A {@link WithSpanFlags} object for optional additional assertions on the flags. + */ + AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position); + } + + private static final RubyText ALREADY_FAILED_WITH_TEXT = + (text, position) -> ALREADY_FAILED_AND_FLAGS; + + private static Factory> rubySpans(Spanned actualSpanned) { + return (FailureMetadata metadata, List spans) -> + new RubySpansSubject(metadata, spans, actualSpanned); + } + + private static final class RubySpansSubject extends Subject implements RubyText { + + private final List actualSpans; + private final Spanned actualSpanned; + + private RubySpansSubject( + FailureMetadata metadata, List actualSpans, Spanned actualSpanned) { + super(metadata, actualSpans); + this.actualSpans = actualSpans; + this.actualSpanned = actualSpanned; + } + + @Override + public AndSpanFlags withTextAndPosition(String text, @RubySpan.Position int position) { + List matchingSpanFlags = new ArrayList<>(); + List spanTextsAndPositions = new ArrayList<>(); + for (RubySpan span : actualSpans) { + spanTextsAndPositions.add(new TextAndPosition(span.rubyText, span.position)); + if (span.rubyText.equals(text)) { + matchingSpanFlags.add(actualSpanned.getSpanFlags(span)); + } + } + check("rubyTextAndPosition") + .that(spanTextsAndPositions) + .containsExactly(new TextAndPosition(text, position)); + return check("flags").about(spanFlags()).that(matchingSpanFlags); + } + + private static final class TextAndPosition { + private final String text; + @RubySpan.Position private final int position; + + private TextAndPosition(String text, int position) { + this.text = text; + this.position = position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TextAndPosition that = (TextAndPosition) o; + if (position != that.position) { + return false; + } + return text.equals(that.text); + } + + @Override + public int hashCode() { + int result = text.hashCode(); + result = 31 * result + position; + return result; + } + + @Override + public String toString() { + return String.format("{text='%s',position=%s}", text, position); + } + } } } diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java index 37ccef6908..32ce419c19 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/truth/SpannedSubjectTest.java @@ -21,12 +21,18 @@ import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.spanne import static com.google.common.truth.ExpectFailure.assertThat; import static com.google.common.truth.ExpectFailure.expectFailureAbout; +import android.graphics.Color; import android.graphics.Typeface; import android.text.SpannableString; import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; +import com.google.android.exoplayer2.text.span.RubySpan; import com.google.common.truth.ExpectFailure; import org.junit.Test; import org.junit.runner.RunWith; @@ -60,7 +66,9 @@ public class SpannedSubjectTest { int end = start + "italic".length(); spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -75,14 +83,21 @@ public class SpannedSubjectTest { whenTesting -> whenTesting .that(spannable) - .hasItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + .hasItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); - assertThat(failure).factKeys().contains("No matching span found"); - assertThat(failure).factValue("in text").isEqualTo(spannable.toString()); - assertThat(failure).factValue("expected").contains("flags=" + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); assertThat(failure) - .factValue("but found") - .contains("flags=" + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + .factValue("value of") + .isEqualTo( + String.format( + "spanned.StyleSpan (start=%s,end=%s,style=%s).contains()", + start, end, Typeface.ITALIC)); + assertThat(failure) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + assertThat(failure) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); } @Test @@ -90,7 +105,10 @@ public class SpannedSubjectTest { AssertionError failure = expectFailure( whenTesting -> - whenTesting.that(null).hasItalicSpan(0, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + whenTesting + .that(null) + .hasItalicSpanBetween(0, 5) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); assertThat(failure).factKeys().containsExactly("Spanned must not be null"); } @@ -102,7 +120,9 @@ public class SpannedSubjectTest { int end = start + "bold".length(); spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -113,7 +133,9 @@ public class SpannedSubjectTest { spannable.setSpan( new StyleSpan(Typeface.BOLD_ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } @Test @@ -124,7 +146,26 @@ public class SpannedSubjectTest { spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasBoldItalicSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasBoldItalicSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void boldItalicSpan_onlyItalic() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasBoldItalicSpanBetween(start, end)); + assertThat(expected) + .factKeys() + .contains( + String.format("No matching StyleSpans found between start=%s,end=%s", start, end)); + assertThat(expected).factValue("but found styles").contains("[" + Typeface.ITALIC + "]"); } @Test @@ -141,8 +182,43 @@ public class SpannedSubjectTest { whenTesting -> whenTesting .that(spannable) - .hasBoldItalicSpan(incorrectStart, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); - assertThat(expected).factValue("expected either").contains("start=" + incorrectStart); + .hasBoldItalicSpanBetween(incorrectStart, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("expected").contains("start=" + incorrectStart); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void noStyleSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then italic spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with underline then ".length(), + "test with underline then italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoStyleSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noStyleSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with italic section"); + int start = "test with ".length(); + int end = start + "italic".length(); + spannable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoStyleSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected StyleSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); assertThat(expected).factValue("but found").contains("start=" + start); } @@ -153,7 +229,593 @@ public class SpannedSubjectTest { int end = start + "underlined".length(); spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - assertThat(spannable).hasUnderlineSpan(start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + assertThat(spannable) + .hasUnderlineSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void noUnderlineSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with italic then underline spans"); + spannable.setSpan( + new StyleSpan(Typeface.ITALIC), + "test with ".length(), + "test with italic".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new UnderlineSpan(), + "test with italic then ".length(), + "test with italic then underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoUnderlineSpanBetween(0, "test with italic then".length()); + } + + @Test + public void noUnderlineSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with underline section"); + int start = "test with ".length(); + int end = start + "underline".length(); + spannable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoUnderlineSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected UnderlineSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void foregroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void foregroundColorSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, incorrectEnd) + .withColor(Color.CYAN)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void foregroundColorSpan_wrongColor() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.BLUE)); + assertThat(expected).factValue("value of").contains("foregroundColor"); + assertThat(expected).factValue("expected").contains("0xFF0000FF"); // Color.BLUE + assertThat(expected).factValue("but was").contains("0xFF00FFFF"); // Color.CYAN + } + + @Test + public void foregroundColorSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasForegroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noForegroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoForegroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noForegroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new ForegroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoForegroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected ForegroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void backgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void backgroundColorSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, incorrectEnd) + .withColor(Color.CYAN)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void backgroundColorSpan_wrongColor() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.BLUE)); + assertThat(expected).factValue("value of").contains("backgroundColor"); + assertThat(expected).factValue("expected").contains("0xFF0000FF"); // Color.BLUE + assertThat(expected).factValue("but was").contains("0xFF00FFFF"); // Color.CYAN + } + + @Test + public void backgroundColorSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasBackgroundColorSpanBetween(start, end) + .withColor(Color.CYAN) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noBackgroundColorSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then cyan spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), + "test with underline then ".length(), + "test with underline then cyan".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoBackgroundColorSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noBackgroundColorSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new BackgroundColorSpan(Color.CYAN), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting.that(spannable).hasNoBackgroundColorSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected BackgroundColorSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void typefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void typefaceSpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, incorrectEnd) + .withFamily("courier")); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void typefaceSpan_wrongFamily() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("roboto")); + assertThat(expected).factValue("value of").contains("family"); + assertThat(expected).factValue("expected").contains("roboto"); + assertThat(expected).factValue("but was").contains("courier"); + } + + @Test + public void typefaceSpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasTypefaceSpanBetween(start, end) + .withFamily("courier") + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noTypefaceSpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then courier spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new TypefaceSpan("courier"), + "test with underline then ".length(), + "test with underline then courier".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoTypefaceSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noTypefaceSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with courier section"); + int start = "test with ".length(); + int end = start + "courier".length(); + spannable.setSpan(new TypefaceSpan("courier"), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoTypefaceSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected TypefaceSpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void rubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void rubySpan_wrongEndIndex() { + SpannableString spannable = SpannableString.valueOf("test with cyan section"); + int start = "test with ".length(); + int end = start + "cyan".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + int incorrectEnd = end + 2; + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, incorrectEnd) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("expected").contains("end=" + incorrectEnd); + assertThat(expected).factValue("but found").contains("end=" + end); + } + + @Test + public void rubySpan_wrongText() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("incorrect text", RubySpan.POSITION_OVER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("text='incorrect text'"); + assertThat(expected).factValue("but was").contains("text='ruby text'"); + } + + @Test + public void rubySpan_wrongPosition() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_UNDER)); + assertThat(expected).factValue("value of").contains("rubyTextAndPosition"); + assertThat(expected).factValue("expected").contains("position=" + RubySpan.POSITION_UNDER); + assertThat(expected).factValue("but was").contains("position=" + RubySpan.POSITION_OVER); + } + + @Test + public void rubySpan_wrongFlags() { + SpannableString spannable = SpannableString.valueOf("test with rubied section"); + int start = "test with ".length(); + int end = start + "rubied".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasRubySpanBetween(start, end) + .withTextAndPosition("ruby text", RubySpan.POSITION_OVER) + .andFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected).factValue("value of").contains("flags"); + assertThat(expected) + .factValue("expected to contain") + .contains(String.valueOf(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)); + assertThat(expected) + .factValue("but was") + .contains(String.valueOf(Spanned.SPAN_INCLUSIVE_EXCLUSIVE)); + } + + @Test + public void noRubySpan_success() { + SpannableString spannable = SpannableString.valueOf("test with underline then ruby spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + "test with underline then ".length(), + "test with underline then ruby".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable).hasNoRubySpanBetween(0, "test with underline then".length()); + } + + @Test + public void noRubySpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with ruby section"); + int start = "test with ".length(); + int end = start + "ruby".length(); + spannable.setSpan( + new RubySpan("ruby text", RubySpan.POSITION_OVER), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> whenTesting.that(spannable).hasNoRubySpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains("Found unexpected RubySpans between start=" + (start + 1) + ",end=" + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); + } + + @Test + public void horizontalTextInVerticalContextSpan_success() { + SpannableString spannable = SpannableString.valueOf("vertical text with horizontal section"); + int start = "vertical text with ".length(); + int end = start + "horizontal".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasHorizontalTextInVerticalContextSpanBetween(start, end) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + + @Test + public void noHorizontalTextInVerticalContextSpan_success() { + SpannableString spannable = + SpannableString.valueOf("test with underline then tate-chu-yoko spans"); + spannable.setSpan( + new UnderlineSpan(), + "test with ".length(), + "test with underline".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), + "test with underline then ".length(), + "test with underline then tate-chu-yoko".length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + assertThat(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(0, "test with underline then".length()); + } + + @Test + public void noHorizontalTextInVerticalContextSpan_failure() { + SpannableString spannable = SpannableString.valueOf("test with tate-chu-yoko section"); + int start = "test with ".length(); + int end = start + "tate-chu-yoko".length(); + spannable.setSpan( + new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + AssertionError expected = + expectFailure( + whenTesting -> + whenTesting + .that(spannable) + .hasNoHorizontalTextInVerticalContextSpanBetween(start + 1, end)); + assertThat(expected) + .factKeys() + .contains( + "Found unexpected HorizontalTextInVerticalContextSpans between start=" + + (start + 1) + + ",end=" + + end); + assertThat(expected).factKeys().contains("expected none"); + assertThat(expected).factValue("but found").contains("start=" + start); } private static AssertionError expectFailure(