Merge branch 'google-dev-v2' into dev-v2-ac4-drm

This commit is contained in:
ybai001 2020-01-09 09:08:45 +08:00
commit 0ea49df901
112 changed files with 4937 additions and 2300 deletions

View file

@ -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 \<ruby\> and \<rt\> 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) ###

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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(

View file

@ -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<Class<?>> 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);
}
}

View file

@ -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 {

View file

@ -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 {
*;

View file

@ -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;
/**

View file

@ -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

View file

@ -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) {

View file

@ -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;
}
}
/**

View file

@ -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;

View file

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

View file

@ -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);

View file

@ -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);

View file

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

View file

@ -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;
}

View file

@ -64,10 +64,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Nullable Constructor<? extends Extractor> 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.

View file

@ -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) {

View file

@ -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);

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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),

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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<byte[]> initializationData = null;
@Nullable List<byte[]> initializationData = null;
// The wide_aspect_ratio flag only has meaning for CEA-708.
if (isDigital) {
boolean isWideAspectRatio = (flags & 0x40) != 0;

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) {
byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
int firstByte = csdData[4] & 0xFF;

View file

@ -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));
}
}
}

View file

@ -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 {
/**

View file

@ -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;
}

View file

@ -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();

View file

@ -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.
* <p>
* If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},
*
* <p>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.
* <p>
* If a frame header is read in full but cannot be parsed then the state is changed to
* {@link #STATE_READING_HEADER}.
* <p>
* 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.
*
* <p>If a frame header is read in full but cannot be parsed then the state is changed to {@link
* #STATE_READING_HEADER}.
*
* <p>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.
* <p>
* If a frame is read in full then true is returned. The frame will have been output, and the
*
* <p>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.
* <p>
* 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.
*
* <p>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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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<DvbSubtitleInfo> 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<DvbSubtitleInfo> dvbSubtitleInfos,
public EsInfo(
int streamType,
@Nullable String language,
@Nullable List<DvbSubtitleInfo> 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 = "";
}
/**

View file

@ -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),

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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 {

View file

@ -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.
*
* <p>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) {

View file

@ -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.

View file

@ -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.

View file

@ -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.
*
* <p>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) {

View file

@ -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

View file

@ -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);

View file

@ -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;
}

View file

@ -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 <a href="https://developer.android.com/guide/topics/text/spans">span
* styling</a>.
*/
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.
*
* <p>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() {}
}

View file

@ -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);

View file

@ -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.
*
* <p>This is used in vertical text to write some characters in a horizontal orientation, known in
* Japanese as tate-chu-yoko.
*
* <p>More information on <a
* href="https://www.w3.org/TR/jlreq/#handling_of_tatechuyoko">tate-chu-yoko</a> and <a
* href="https://developer.android.com/guide/topics/text/spans">span styling</a>.
*/
// 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 {}

View file

@ -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.
*
* <p>The text covered by this span is known as the "base text", and the ruby text is stored in
* {@link #rubyText}.
*
* <p>More information on <a href="https://en.wikipedia.org/wiki/Ruby_character">ruby characters</a>
* and <a href="https://developer.android.com/guide/topics/text/spans">span styling</a>.
*/
// 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 <rp> 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.
*
* <p>For vertical text it should be positioned to the right, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_OVER = 1;
/**
* The ruby text should be positioned below the base text.
*
* <p>For vertical text it should be positioned to the left, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_UNDER = 2;
/**
* The possible positions of the ruby text relative to the base text.
*
* <p>One of:
*
* <ul>
* <li>{@link #POSITION_UNKNOWN}
* <li>{@link #POSITION_OVER}
* <li>{@link #POSITION_UNDER}
* </ul>
*/
@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;
}
}

View file

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

View file

@ -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);

View file

@ -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(

View file

@ -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<StartTag> startTagStack = new ArrayDeque<>();
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
int pos = 0;
List<Element> 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<Element> nestedElements,
SpannableStringBuilder text,
List<WebvttCssStyle> styles,
List<StyleMatch> 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 <rt> tags inside <ruby> 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;
}
}
}

View file

@ -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<TrackSelection.Factory, LoadControl> 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<? extends MediaChunk> 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;
}
}
}

View file

@ -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;
}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <lang.class1.class2> courier and violet.
This should be underlined and <lang.class1.class2>courier and violet.
íd
00:02.000 --> 00:02.001
@ -31,10 +31,10 @@ _id
This <lang.class.another>should be courier and bold.
00:04.000 --> 00:04.001
This <v Strider Trancos> shouldn't be bold.</v>
This <v.class.clazz Strider Trancos> should be bold.
This <v Strider Trancos>shouldn't be bold.</v>
This <v.class.clazz Strider Trancos>should be bold.
anId
00:05.000 --> 00:05.001
This is <v.class1.class3.class2 Pipo> specific </v>
<v.class1.class3.class2 Robert> But this is more italic</v>
This is <v.class1.class3.class2 Pipo>specific</v>
<v.class1.class3.class2 Robert>But this is more italic</v>

View file

@ -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 <c.tcu-all>all</c> test
00:03.000 --> 00:04.000 vertical:rl
Combine <c.tcu-digits>0004</c> digits

View file

@ -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.

View file

@ -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}.
*
* <p>Some expected results in these tests have been retrieved using the <a
* href="https://xiph.org/flac/documentation_tools_flac.html">flac</a> 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;
}
}

View file

@ -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}.
*
* <p>Most expected results in these tests have been retrieved using the <a
* href="https://xiph.org/flac/documentation_tools_metaflac.html">metaflac</a> 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<>());
}
}

View file

@ -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);
}
}

View file

@ -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<Format> closedCaptionFormats) {
return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats);
}

View file

@ -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");
}
}

View file

@ -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) {

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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 {

Some files were not shown because too many files have changed in this diff Show more