Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into google-dev-v2

This commit is contained in:
ybai001 2020-01-09 09:08:07 +08:00
commit 65e6d50e73
112 changed files with 4943 additions and 2290 deletions

View file

@ -32,6 +32,30 @@
([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling ([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling
could previously cause downloads to be paused when they should have been able could previously cause downloads to be paused when they should have been able
to proceed. 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) ### ### 2.11.1 (2019-12-20) ###

View file

@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
Assertions.checkNotNull(format.sampleMimeType); Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) {
|| !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;

View file

@ -64,9 +64,7 @@ import java.util.List;
throw new FfmpegDecoderException("Failed to load decoder native libraries."); throw new FfmpegDecoderException("Failed to load decoder native libraries.");
} }
Assertions.checkNotNull(format.sampleMimeType); Assertions.checkNotNull(format.sampleMimeType);
codecName = codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
Assertions.checkNotNull(
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
extraData = getExtraData(format.sampleMimeType, format.initializationData); extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
@ -145,16 +143,12 @@ import java.util.List;
nativeContext = 0; nativeContext = 0;
} }
/** /** Returns the channel count of output audio. */
* Returns the channel count of output audio. May only be called after {@link #decode}.
*/
public int getChannelCount() { public int getChannelCount() {
return channelCount; return channelCount;
} }
/** /** Returns the sample rate of output audio. */
* Returns the sample rate of output audio. May only be called after {@link #decode}.
*/
public int getSampleRate() { public int getSampleRate() {
return sampleRate; return sampleRate;
} }

View file

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Log; 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. * Returns whether the underlying library supports the specified MIME type.
* *
* @param mimeType The MIME type to check. * @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()) { if (!isAvailable()) {
return false; return false;
} }
String codecName = getCodecName(mimeType, encoding); String codecName = getCodecName(mimeType);
if (codecName == null) { if (codecName == null) {
return false; 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} * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
* if it's unsupported. * if it's unsupported.
*/ */
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { /* package */ static @Nullable String getCodecName(String mimeType) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
return "aac"; return "aac";
@ -116,14 +114,10 @@ public final class FfmpegLibrary {
return "flac"; return "flac";
case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_ALAC:
return "alac"; return "alac";
case MimeTypes.AUDIO_RAW: case MimeTypes.AUDIO_MLAW:
if (encoding == C.ENCODING_PCM_MU_LAW) { return "pcm_mulaw";
return "pcm_mulaw"; case MimeTypes.AUDIO_ALAW:
} else if (encoding == C.ENCODING_PCM_A_LAW) { return "pcm_alaw";
return "pcm_alaw";
} else {
return null;
}
default: default:
return null; return null;
} }

View file

@ -126,6 +126,8 @@ import java.nio.ByteBuffer;
if (targetSampleInLastFrame) { if (targetSampleInLastFrame) {
// We are holding the target frame in outputFrameHolder. Set its presentation time now. // We are holding the target frame in outputFrameHolder. Set its presentation time now.
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); 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()); return TimestampSearchResult.targetFoundResult(input.getPosition());
} else if (nextFrameSampleIndex <= targetSampleIndex) { } else if (nextFrameSampleIndex <= targetSampleIndex) {
return TimestampSearchResult.underestimatedResult( 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 testImplementation 'org.robolectric:robolectric:' + robolectricVersion
// Do not update to 3.13.X or later until minSdkVersion is increased to 21: // 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 // 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 // Since OkHttp is distributed as a jar rather than an aar, Gradle won't
// us from making this mistake! // stop us from making this mistake!
api 'com.squareup.okhttp3:okhttp:3.12.5' api 'com.squareup.okhttp3:okhttp:3.12.7'
} }
ext { ext {

View file

@ -5,6 +5,12 @@
public static android.net.Uri buildRawResourceUri(int); 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. # Some members of this class are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { -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}, * 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 * {@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_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_MP3}, {@link * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link
* #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
* {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -162,11 +162,10 @@ public final class C {
ENCODING_INVALID, ENCODING_INVALID,
ENCODING_PCM_8BIT, ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_16BIT_BIG_ENDIAN,
ENCODING_PCM_24BIT, ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT, ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT, ENCODING_PCM_FLOAT,
ENCODING_PCM_MU_LAW,
ENCODING_PCM_A_LAW,
ENCODING_MP3, ENCODING_MP3,
ENCODING_AC3, ENCODING_AC3,
ENCODING_E_AC3, ENCODING_E_AC3,
@ -174,15 +173,15 @@ public final class C {
ENCODING_AC4, ENCODING_AC4,
ENCODING_DTS, ENCODING_DTS,
ENCODING_DTS_HD, ENCODING_DTS_HD,
ENCODING_DOLBY_TRUEHD, ENCODING_DOLBY_TRUEHD
}) })
public @interface Encoding {} public @interface Encoding {}
/** /**
* Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * 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 * {@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_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. * {@link #ENCODING_PCM_FLOAT}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -191,11 +190,10 @@ public final class C {
ENCODING_INVALID, ENCODING_INVALID,
ENCODING_PCM_8BIT, ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_16BIT_BIG_ENDIAN,
ENCODING_PCM_24BIT, ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT, ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT, ENCODING_PCM_FLOAT
ENCODING_PCM_MU_LAW,
ENCODING_PCM_A_LAW
}) })
public @interface PcmEncoding {} public @interface PcmEncoding {}
/** @see AudioFormat#ENCODING_INVALID */ /** @see AudioFormat#ENCODING_INVALID */
@ -204,16 +202,14 @@ public final class C {
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/** @see AudioFormat#ENCODING_PCM_16BIT */ /** @see AudioFormat#ENCODING_PCM_16BIT */
public static final int ENCODING_PCM_16BIT = 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. */ /** 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. */ /** 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 */ /** @see AudioFormat#ENCODING_PCM_FLOAT */
public static final int ENCODING_PCM_FLOAT = 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 */ /** @see AudioFormat#ENCODING_MP3 */
public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
/** @see AudioFormat#ENCODING_AC3 */ /** @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}, * 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 * {@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 * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link
* {@link #NETWORK_TYPE_OTHER}. * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -993,6 +989,7 @@ public final class C {
NETWORK_TYPE_2G, NETWORK_TYPE_2G,
NETWORK_TYPE_3G, NETWORK_TYPE_3G,
NETWORK_TYPE_4G, NETWORK_TYPE_4G,
NETWORK_TYPE_5G,
NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_CELLULAR_UNKNOWN,
NETWORK_TYPE_ETHERNET, NETWORK_TYPE_ETHERNET,
NETWORK_TYPE_OTHER NETWORK_TYPE_OTHER
@ -1010,6 +1007,8 @@ public final class C {
public static final int NETWORK_TYPE_3G = 4; public static final int NETWORK_TYPE_3G = 4;
/** Network type for a 4G cellular connection. */ /** Network type for a 4G cellular connection. */
public static final int NETWORK_TYPE_4G = 5; 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 for cellular connections which cannot be mapped to one of {@link
* #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. * #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; public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;
/** Network type for an Ethernet connection. */ /** Network type for an Ethernet connection. */
public static final int NETWORK_TYPE_ETHERNET = 7; public static final int NETWORK_TYPE_ETHERNET = 7;
/** /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */
* Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,
* Bluetooth).
*/
public static final int NETWORK_TYPE_OTHER = 8; 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. * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
*/ */
public final int sampleRate; public final int sampleRate;
/** /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */
* 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.
*/
public final @C.PcmEncoding int pcmEncoding; public final @C.PcmEncoding int pcmEncoding;
/** /**
* The number of frames to trim from the start of the decoded audio stream, or 0 if not * 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; @Player.State private int playbackState;
private boolean isSuppressed; private boolean isSuppressed;
private float playbackSpeed; private float playbackSpeed;
private boolean isSeeking;
/** /**
* Creates listener for playback stats. * Creates listener for playback stats.
@ -169,6 +170,9 @@ public final class PlaybackStatsListener
@Override @Override
public void onSessionCreated(EventTime eventTime, String session) { public void onSessionCreated(EventTime eventTime, String session) {
PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
if (isSeeking) {
tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true);
}
tracker.onPlayerStateChanged( tracker.onPlayerStateChanged(
eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true);
tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true);
@ -288,20 +292,20 @@ public final class PlaybackStatsListener
public void onSeekStarted(EventTime eventTime) { public void onSeekStarted(EventTime eventTime) {
sessionManager.updateSessions(eventTime); sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) { for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) { boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers.get(session).onSeekStarted(eventTime); playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback);
}
} }
isSeeking = true;
} }
@Override @Override
public void onSeekProcessed(EventTime eventTime) { public void onSeekProcessed(EventTime eventTime) {
sessionManager.updateSessions(eventTime); sessionManager.updateSessions(eventTime);
for (String session : playbackStatsTrackers.keySet()) { for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) { boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers.get(session).onSeekProcessed(eventTime); playbackStatsTrackers.get(session).onSeekProcessed(eventTime, belongsToPlayback);
}
} }
isSeeking = false;
} }
@Override @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 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; 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 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; isSeeking = false;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); maybeUpdatePlaybackState(eventTime, belongsToPlayback);
} }
/** /**
@ -875,7 +883,7 @@ public final class PlaybackStatsListener
return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED
? PlaybackStats.PLAYBACK_STATE_ENDED ? PlaybackStats.PLAYBACK_STATE_ENDED
: PlaybackStats.PLAYBACK_STATE_ABANDONED; : 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. // Seeking takes precedence over errors such that we report a seek while in error state.
return PlaybackStats.PLAYBACK_STATE_SEEKING; return PlaybackStats.PLAYBACK_STATE_SEEKING;
} else if (hasFatalError) { } 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 * 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 { public final class Ac3Util {
@ -39,8 +39,8 @@ public final class Ac3Util {
public static final class SyncFrameInfo { 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}, * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link
* {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -114,9 +114,7 @@ public final class Ac3Util {
* The number of new samples per (E-)AC-3 audio block. * The number of new samples per (E-)AC-3 audio block.
*/ */
private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;
/** /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */
* Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.
*/
private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; 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. * 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. * Channel counts, indexed by acmod.
*/ */
private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; 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 table 4.13.) */
* 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[] {
private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640
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.) */
* 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[] {
private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, 69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253,
121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; 1393
};
/** /**
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F.
* 102 366 Annex F. The reading position of {@code data} will be modified. * The reading position of {@code data} will be modified.
* *
* @param data The AC3SpecificBox to parse. * @param data The AC3SpecificBox to parse.
* @param trackId The track identifier to set on the format. * @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 * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex
* 102 366 Annex F. The reading position of {@code data} will be modified. * F. The reading position of {@code data} will be modified.
* *
* @param data The EC3SpecificBox to parse. * @param data The EC3SpecificBox to parse.
* @param trackId The track identifier to set on the format. * @param trackId The track identifier to set on the format.
@ -243,9 +242,10 @@ public final class Ac3Util {
public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
int initialPosition = data.getPosition(); int initialPosition = data.getPosition();
data.skipBits(40); 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); data.setPosition(initialPosition);
String mimeType; @Nullable String mimeType;
@StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
int sampleRate; int sampleRate;
int acmod; int acmod;
@ -254,7 +254,7 @@ public final class Ac3Util {
boolean lfeon; boolean lfeon;
int channelCount; int channelCount;
if (isEac3) { 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 data.skipBits(16); // syncword
switch (data.readBits(2)) { // strmtyp switch (data.readBits(2)) { // strmtyp
case 0: case 0:
@ -472,7 +472,8 @@ public final class Ac3Util {
if (data.length < 6) { if (data.length < 6) {
return C.LENGTH_UNSET; 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) { if (isEac3) {
int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits.
frmsiz |= data[3] & 0xFF; // Least significant 8 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. * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's
*/
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
* position is not modified. * position is not modified.
* *
* @param buffer The {@link ByteBuffer} from which to read the syncframe. * @param buffer The {@link ByteBuffer} from which to read the syncframe.
* @return The number of audio samples represented by the syncframe. * @return The number of audio samples represented by the syncframe.
*/ */
public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
// See ETSI TS 102 366 subsection E.1.2.2. // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10;
return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 if (isEac3) {
: BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); 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. */ /** The channel count of AC-4 stream. */
// TODO: Parse AC-4 stream channel count. // TODO: Parse AC-4 stream channel count.
private static final int CHANNEL_COUNT_2 = 2; 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 * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full
* header size. * 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. */ /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */
public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { public static void getAc4SampleHeader(int size, ParsableByteArray buffer) {
// See ETSI TS 103 190-1 V1.3.1, Annex G. // 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[0] = (byte) 0xAC;
buffer.data[1] = 0x40; buffer.data[1] = 0x40;
buffer.data[2] = (byte) 0xFF; 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_24BIT:
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_MU_LAW:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
throw new IllegalArgumentException(); throw new IllegalArgumentException();
@ -1166,10 +1164,9 @@ public final class DefaultAudioSink implements AudioSink {
case C.ENCODING_DTS_HD: case C.ENCODING_DTS_HD:
return DtsUtil.parseDtsAudioSampleCount(buffer); return DtsUtil.parseDtsAudioSampleCount(buffer);
case C.ENCODING_AC3: case C.ENCODING_AC3:
return Ac3Util.getAc3SyncframeAudioSampleCount();
case C.ENCODING_E_AC3: case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC: case C.ENCODING_E_AC3_JOC:
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer);
case C.ENCODING_AC4: case C.ENCODING_AC4:
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
case C.ENCODING_DOLBY_TRUEHD: 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. * @return The DTS format parsed from data in the header.
*/ */
public static Format parseDtsFormat( 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); ParsableBitArray frameBits = getNormalizedFrameHeader(frame);
frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
int amode = frameBits.readBits(6); 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 int MAX_PENDING_STREAM_CHANGE_COUNT = 10;
private static final String TAG = "MediaCodecAudioRenderer"; 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 Context context;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
@ -566,7 +571,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
mediaFormat.getString(MediaFormat.KEY_MIME)); mediaFormat.getString(MediaFormat.KEY_MIME));
} else { } else {
mediaFormat = outputMediaFormat; 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 channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);

View file

@ -29,8 +29,11 @@ import java.nio.ByteBuffer;
public AudioFormat onConfigure(AudioFormat inputAudioFormat) public AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException { throws UnhandledAudioFormatException {
@C.PcmEncoding int encoding = inputAudioFormat.encoding; @C.PcmEncoding int encoding = inputAudioFormat.encoding;
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT if (encoding != C.ENCODING_PCM_8BIT
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { && 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); throw new UnhandledAudioFormatException(inputAudioFormat);
} }
return encoding != C.ENCODING_PCM_16BIT return encoding != C.ENCODING_PCM_16BIT
@ -50,6 +53,9 @@ import java.nio.ByteBuffer;
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
resampledSize = size * 2; resampledSize = size * 2;
break; break;
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
resampledSize = size;
break;
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:
resampledSize = (size / 3) * 2; resampledSize = (size / 3) * 2;
break; break;
@ -58,8 +64,6 @@ import java.nio.ByteBuffer;
break; break;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
@ -70,21 +74,28 @@ import java.nio.ByteBuffer;
ByteBuffer buffer = replaceOutputBuffer(resampledSize); ByteBuffer buffer = replaceOutputBuffer(resampledSize);
switch (inputAudioFormat.encoding) { switch (inputAudioFormat.encoding) {
case C.ENCODING_PCM_8BIT: 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++) { for (int i = position; i < limit; i++) {
buffer.put((byte) 0); buffer.put((byte) 0);
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
} }
break; 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: 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) { for (int i = position; i < limit; i += 3) {
buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 1));
buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 2));
} }
break; break;
case C.ENCODING_PCM_32BIT: 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) { for (int i = position; i < limit; i += 4) {
buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 2));
buffer.put(inputBuffer.get(i + 3)); buffer.put(inputBuffer.get(i + 3));
@ -92,8 +103,6 @@ import java.nio.ByteBuffer;
break; break;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:

View file

@ -32,15 +32,17 @@ public final class WavUtil {
public static final int DATA_FOURCC = 0x64617461; public static final int DATA_FOURCC = 0x64617461;
/** WAVE type value for integer PCM audio data. */ /** 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. */ /** 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. */ /** 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. */ /** 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. */ /** 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}. * 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_24BIT:
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
return TYPE_PCM; 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: case C.ENCODING_PCM_FLOAT:
return TYPE_FLOAT; return TYPE_FLOAT;
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
@ -81,10 +79,6 @@ public final class WavUtil {
return Util.getPcmEncoding(bitsPerSample); return Util.getPcmEncoding(bitsPerSample);
case TYPE_FLOAT: case TYPE_FLOAT:
return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; 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: default:
return C.ENCODING_INVALID; return C.ENCODING_INVALID;
} }

View file

@ -64,10 +64,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Nullable Constructor<? extends Extractor> flacExtensionExtractorConstructor = null; @Nullable Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
try { try {
// LINT.IfChange // LINT.IfChange
flacExtensionExtractorConstructor = @SuppressWarnings("nullness:argument.type.incompatible")
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") boolean isFlacNativeLibraryAvailable =
.asSubclass(Extractor.class) Boolean.TRUE.equals(
.getConstructor(); 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) // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
// Expected if the app was built without the FLAC extension. // 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 * @param data The array to read the data from, whose position must correspond to the block size
* bits. * bits.
* @param blockSizeKey The key in the block size lookup table. * @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) { public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) {
switch (blockSizeKey) { switch (blockSizeKey) {

View file

@ -256,15 +256,18 @@ public final class FlacExtractor implements Extractor {
// Copy more bytes into the buffer. // Copy more bytes into the buffer.
int currentLimit = buffer.limit(); int currentLimit = buffer.limit();
int bytesRead = boolean foundEndOfInput = false;
input.read( if (currentLimit < BUFFER_LENGTH) {
buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); int bytesRead =
boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; input.read(
if (!foundEndOfInput) { buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit);
buffer.setLimit(currentLimit + bytesRead); foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT;
} else if (buffer.bytesLeft() == 0) { if (!foundEndOfInput) {
outputSampleMetadata(); buffer.setLimit(currentLimit + bytesRead);
return Extractor.RESULT_END_OF_INPUT; } else if (buffer.bytesLeft() == 0) {
outputSampleMetadata();
return Extractor.RESULT_END_OF_INPUT;
}
} }
// Search for a frame. // 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. // Skip frame search on the bytes within the minimum frame size.
if (currentFrameBytesWritten < minFrameSize) { if (currentFrameBytesWritten < minFrameSize) {
buffer.skipBytes(Math.min(minFrameSize, buffer.bytesLeft())); buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft()));
} }
long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); 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) { } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
: MimeTypes.AUDIO_MLAW; : MimeTypes.AUDIO_MLAW;
int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; Format format =
Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, Format.createAudioSampleFormat(
Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); /* 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); output.format(format);
hasOutputFormat = true; hasOutputFormat = true;
} else if (audioFormat != AUDIO_FORMAT_AAC) { } 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 (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {
if (blockSampleCount > 1) { if (blockSampleCount > 1) {
Log.w(TAG, "Skipping subtitle sample in laced block."); 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."); Log.w(TAG, "Skipping subtitle sample with no duration.");
} else { } 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 // Note: If we ever want to support DRM protected subtitles then we'll need to output the
// appropriate encryption data here. // appropriate encryption data here.
track.output.sampleData(subtitleSample, subtitleSample.limit()); track.output.sampleData(subtitleSample, subtitleSample.limit());
@ -1829,10 +1829,8 @@ public class MatroskaExtractor implements Extractor {
chunkSize += size; chunkSize += size;
chunkOffset = offset; // The offset is to the end of the sample. chunkOffset = offset; // The offset is to the end of the sample.
if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {
// We haven't read enough samples to output a chunk. outputPendingSampleMetadata(track);
return;
} }
outputPendingSampleMetadata(track);
} }
public void outputPendingSampleMetadata(Track track) { public void outputPendingSampleMetadata(Track track) {

View file

@ -379,6 +379,9 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants") @SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_dfLa = 0x64664c61; public static final int TYPE_dfLa = 0x64664c61;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_twos = 0x74776f73;
public final int type; public final int type;
public Atom(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_sawb
|| childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_lpcm
|| childAtomType == Atom.TYPE_sowt || childAtomType == Atom.TYPE_sowt
|| childAtomType == Atom.TYPE_twos
|| childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw || childAtomType == Atom.TYPE_alaw
@ -1086,6 +1087,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int channelCount; int channelCount;
int sampleRate; int sampleRate;
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
channelCount = parent.readUnsignedShort(); channelCount = parent.readUnsignedShort();
@ -1147,6 +1149,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
mimeType = MimeTypes.AUDIO_AMR_WB; mimeType = MimeTypes.AUDIO_AMR_WB;
} else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
mimeType = MimeTypes.AUDIO_RAW; 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) { } else if (atomType == Atom.TYPE__mp3) {
mimeType = MimeTypes.AUDIO_MPEG; mimeType = MimeTypes.AUDIO_MPEG;
} else if (atomType == Atom.TYPE_alac) { } else if (atomType == Atom.TYPE_alac) {
@ -1233,9 +1239,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
if (out.format == null && mimeType != null) { 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, out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
initializationData == null ? null : Collections.singletonList(initializationData), initializationData == null ? null : Collections.singletonList(initializationData),

View file

@ -168,7 +168,6 @@ public class FragmentedMp4Extractor implements Extractor {
private int sampleBytesWritten; private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining; private int sampleCurrentNalBytesRemaining;
private boolean processSeiNalUnitPayload; private boolean processSeiNalUnitPayload;
private boolean isAc4HeaderRequired;
// Extractor output. // Extractor output.
@MonotonicNonNull private ExtractorOutput extractorOutput; @MonotonicNonNull private ExtractorOutput extractorOutput;
@ -302,7 +301,6 @@ public class FragmentedMp4Extractor implements Extractor {
pendingMetadataSampleBytes = 0; pendingMetadataSampleBytes = 0;
pendingSeekTimeUs = timeUs; pendingSeekTimeUs = timeUs;
containerAtoms.clear(); containerAtoms.clear();
isAc4HeaderRequired = false;
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} }
@ -1270,11 +1268,18 @@ public class FragmentedMp4Extractor implements Extractor {
} }
sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData();
sampleSize += sampleBytesWritten; sampleSize += sampleBytesWritten;
<<<<<<< HEAD
outputSampleEncryptionDataSize = 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;
}
>>>>>>> 4f15cfaa78f8053c4bf83ef0871df98d29fa6e0b
parserState = STATE_READING_SAMPLE_CONTINUE; parserState = STATE_READING_SAMPLE_CONTINUE;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
isAc4HeaderRequired =
MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType);
} }
TrackFragment fragment = currentTrackBundle.fragment; TrackFragment fragment = currentTrackBundle.fragment;
@ -1339,6 +1344,7 @@ public class FragmentedMp4Extractor implements Extractor {
} }
} }
} else { } else {
<<<<<<< HEAD
if (isAc4HeaderRequired) { if (isAc4HeaderRequired) {
Ac4Util.getAc4SampleHeader(sampleSize - outputSampleEncryptionDataSize, scratch); Ac4Util.getAc4SampleHeader(sampleSize - outputSampleEncryptionDataSize, scratch);
int length = scratch.limit(); int length = scratch.limit();
@ -1347,6 +1353,8 @@ public class FragmentedMp4Extractor implements Extractor {
sampleBytesWritten += length; sampleBytesWritten += length;
isAc4HeaderRequired = false; isAc4HeaderRequired = false;
} }
=======
>>>>>>> 4f15cfaa78f8053c4bf83ef0871df98d29fa6e0b
while (sampleBytesWritten < sampleSize) { while (sampleBytesWritten < sampleSize) {
int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);
sampleBytesWritten += writtenBytes; sampleBytesWritten += writtenBytes;

View file

@ -110,9 +110,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
@Nullable private ParsableByteArray atomData; @Nullable private ParsableByteArray atomData;
private int sampleTrackIndex; private int sampleTrackIndex;
private int sampleBytesRead;
private int sampleBytesWritten; private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining; private int sampleCurrentNalBytesRemaining;
private boolean isAc4HeaderRequired;
// Extractor outputs. // Extractor outputs.
@MonotonicNonNull private ExtractorOutput extractorOutput; @MonotonicNonNull private ExtractorOutput extractorOutput;
@ -160,9 +160,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
containerAtoms.clear(); containerAtoms.clear();
atomHeaderBytesRead = 0; atomHeaderBytesRead = 0;
sampleTrackIndex = C.INDEX_UNSET; sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0;
sampleBytesWritten = 0; sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
isAc4HeaderRequired = false;
if (position == 0) { if (position == 0) {
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} else if (tracks != null) { } else if (tracks != null) {
@ -507,15 +507,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (sampleTrackIndex == C.INDEX_UNSET) { if (sampleTrackIndex == C.INDEX_UNSET) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
isAc4HeaderRequired =
MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType);
} }
Mp4Track track = tracks[sampleTrackIndex]; Mp4Track track = tracks[sampleTrackIndex];
TrackOutput trackOutput = track.trackOutput; TrackOutput trackOutput = track.trackOutput;
int sampleIndex = track.sampleIndex; int sampleIndex = track.sampleIndex;
long position = track.sampleTable.offsets[sampleIndex]; long position = track.sampleTable.offsets[sampleIndex];
int sampleSize = track.sampleTable.sizes[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) { if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = position; positionHolder.position = position;
return RESULT_SEEK; return RESULT_SEEK;
@ -543,6 +541,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (sampleCurrentNalBytesRemaining == 0) { if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one. // Read the NAL length so that we know where we find the next one.
input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
sampleBytesRead += nalUnitLengthFieldLength;
nalLength.setPosition(0); nalLength.setPosition(0);
int nalLengthInt = nalLength.readInt(); int nalLengthInt = nalLength.readInt();
if (nalLengthInt < 0) { if (nalLengthInt < 0) {
@ -557,21 +556,23 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} else { } else {
// Write the payload of the NAL unit. // Write the payload of the NAL unit.
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes; sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes;
} }
} }
} else { } else {
if (isAc4HeaderRequired) { if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) {
Ac4Util.getAc4SampleHeader(sampleSize, scratch); if (sampleBytesWritten == 0) {
int length = scratch.limit(); Ac4Util.getAc4SampleHeader(sampleSize, scratch);
trackOutput.sampleData(scratch, length); trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
sampleSize += length; sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
sampleBytesWritten += length; }
isAc4HeaderRequired = false; sampleSize += Ac4Util.SAMPLE_HEADER_SIZE;
} }
while (sampleBytesWritten < sampleSize) { while (sampleBytesWritten < sampleSize) {
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes; sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes;
} }
@ -580,6 +581,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
track.sampleIndex++; track.sampleIndex++;
sampleTrackIndex = C.INDEX_UNSET; sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0;
sampleBytesWritten = 0; sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
return RESULT_CONTINUE; return RESULT_CONTINUE;

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.Ac3Util; 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; 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. * 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 ParsableBitArray headerScratchBits;
private final ParsableByteArray headerScratchBytes; private final ParsableByteArray headerScratchBytes;
private final String language; @Nullable private final String language;
private String trackFormatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
@State private int state; @State private int state;
private int bytesRead; private int bytesRead;
@ -60,7 +65,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
// Used when parsing the header. // Used when parsing the header.
private long sampleDurationUs; private long sampleDurationUs;
private Format format; @MonotonicNonNull private Format format;
private int sampleSize; private int sampleSize;
// Used when reading the samples. // Used when reading the samples.
@ -78,7 +83,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
* *
* @param language Track language. * @param language Track language.
*/ */
public Ac3Reader(String language) { public Ac3Reader(@Nullable String language) {
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
headerScratchBytes = new ParsableByteArray(headerScratchBits.data); headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
@ -95,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
@Override @Override
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
generator.generateNewId(); generator.generateNewId();
trackFormatId = generator.getFormatId(); formatId = generator.getFormatId();
output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
} }
@ -106,6 +111,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SYNC: case STATE_FINDING_SYNC:
@ -185,19 +191,28 @@ public final class Ac3Reader implements ElementaryStreamReader {
return false; return false;
} }
/** /** Parses the sample header. */
* Parses the sample header. @RequiresNonNull("output")
*/
@SuppressWarnings("ReferenceEquality")
private void parseHeader() { private void parseHeader() {
headerScratchBits.setPosition(0); headerScratchBits.setPosition(0);
SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits); SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits);
if (format == null || frameInfo.channelCount != format.channelCount if (format == null
|| frameInfo.channelCount != format.channelCount
|| frameInfo.sampleRate != format.sampleRate || frameInfo.sampleRate != format.sampleRate
|| frameInfo.mimeType != format.sampleMimeType) { || Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) {
format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null, format =
Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null, Format.createAudioSampleFormat(
null, 0, language); formatId,
frameInfo.mimeType,
null,
Format.NO_VALUE,
Format.NO_VALUE,
frameInfo.channelCount,
frameInfo.sampleRate,
null,
null,
0,
language);
output.format(format); output.format(format);
} }
sampleSize = frameInfo.frameSize; sampleSize = frameInfo.frameSize;

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.Ac4Util; 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; 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. */ /** Parses a continuous AC-4 byte stream and extracts individual samples. */
public final class Ac4Reader implements ElementaryStreamReader { public final class Ac4Reader implements ElementaryStreamReader {
@ -44,10 +48,10 @@ public final class Ac4Reader implements ElementaryStreamReader {
private final ParsableBitArray headerScratchBits; private final ParsableBitArray headerScratchBits;
private final ParsableByteArray headerScratchBytes; private final ParsableByteArray headerScratchBytes;
private final String language; @Nullable private final String language;
private String trackFormatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
@State private int state; @State private int state;
private int bytesRead; private int bytesRead;
@ -58,7 +62,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
// Used when parsing the header. // Used when parsing the header.
private long sampleDurationUs; private long sampleDurationUs;
private Format format; @MonotonicNonNull private Format format;
private int sampleSize; private int sampleSize;
// Used when reading the samples. // Used when reading the samples.
@ -74,7 +78,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
* *
* @param language Track language. * @param language Track language.
*/ */
public Ac4Reader(String language) { public Ac4Reader(@Nullable String language) {
headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]); headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]);
headerScratchBytes = new ParsableByteArray(headerScratchBits.data); headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
@ -95,7 +99,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
@Override @Override
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
generator.generateNewId(); generator.generateNewId();
trackFormatId = generator.getFormatId(); formatId = generator.getFormatId();
output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
} }
@ -106,6 +110,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SYNC: case STATE_FINDING_SYNC:
@ -185,7 +190,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
} }
/** Parses the sample header. */ /** Parses the sample header. */
@SuppressWarnings("ReferenceEquality") @RequiresNonNull("output")
private void parseHeader() { private void parseHeader() {
headerScratchBits.setPosition(0); headerScratchBits.setPosition(0);
SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits); SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits);
@ -195,7 +200,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|| !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) { || !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) {
format = format =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
trackFormatId, formatId,
MimeTypes.AUDIO_AC4, MimeTypes.AUDIO_AC4,
/* codecs= */ null, /* codecs= */ null,
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; 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. * 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 boolean exposeId3;
private final ParsableBitArray adtsScratch; private final ParsableBitArray adtsScratch;
private final ParsableByteArray id3HeaderBuffer; private final ParsableByteArray id3HeaderBuffer;
private final String language; @Nullable private final String language;
private String formatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
private TrackOutput id3Output; @MonotonicNonNull private TrackOutput id3Output;
private int state; private int state;
private int bytesRead; private int bytesRead;
@ -90,7 +96,7 @@ public final class AdtsReader implements ElementaryStreamReader {
// Used when reading the samples. // Used when reading the samples.
private long timeUs; private long timeUs;
private TrackOutput currentOutput; @MonotonicNonNull private TrackOutput currentOutput;
private long currentSampleDuration; private long currentSampleDuration;
/** /**
@ -104,7 +110,7 @@ public final class AdtsReader implements ElementaryStreamReader {
* @param exposeId3 True if the reader should expose ID3 information. * @param exposeId3 True if the reader should expose ID3 information.
* @param language Track language. * @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]); adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
setFindingSampleState(); setFindingSampleState();
@ -130,6 +136,7 @@ public final class AdtsReader implements ElementaryStreamReader {
idGenerator.generateNewId(); idGenerator.generateNewId();
formatId = idGenerator.getFormatId(); formatId = idGenerator.getFormatId();
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
currentOutput = output;
if (exposeId3) { if (exposeId3) {
idGenerator.generateNewId(); idGenerator.generateNewId();
id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
@ -147,6 +154,7 @@ public final class AdtsReader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) throws ParserException { public void consume(ParsableByteArray data) throws ParserException {
assertTracksCreated();
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SAMPLE: case STATE_FINDING_SAMPLE:
@ -425,9 +433,8 @@ public final class AdtsReader implements ElementaryStreamReader {
return true; return true;
} }
/** /** Parses the Id3 header. */
* Parses the Id3 header. @RequiresNonNull("id3Output")
*/
private void parseId3Header() { private void parseId3Header() {
id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);
id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);
@ -435,9 +442,8 @@ public final class AdtsReader implements ElementaryStreamReader {
id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);
} }
/** /** Parses the sample header. */
* Parses the sample header. @RequiresNonNull("output")
*/
private void parseAdtsHeader() throws ParserException { private void parseAdtsHeader() throws ParserException {
adtsScratch.setPosition(0); adtsScratch.setPosition(0);
@ -487,9 +493,8 @@ public final class AdtsReader implements ElementaryStreamReader {
setReadingSampleState(output, sampleDurationUs, 0, sampleSize); setReadingSampleState(output, sampleDurationUs, 0, sampleSize);
} }
/** /** Reads the rest of the sample */
* Reads the rest of the sample @RequiresNonNull("currentOutput")
*/
private void readSample(ParsableByteArray data) { private void readSample(ParsableByteArray data) {
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
currentOutput.sampleData(data, bytesToRead); 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 android.util.SparseArray;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.text.cea.Cea708InitializationData; import com.google.android.exoplayer2.text.cea.Cea708InitializationData;
@ -134,6 +135,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
return new SparseArray<>(); return new SparseArray<>();
} }
@Nullable
@Override @Override
public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
switch (streamType) { switch (streamType) {
@ -247,7 +249,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
// Skip reserved (8). // Skip reserved (8).
scratchDescriptorData.skipBytes(1); scratchDescriptorData.skipBytes(1);
List<byte[]> initializationData = null; @Nullable List<byte[]> initializationData = null;
// The wide_aspect_ratio flag only has meaning for CEA-708. // The wide_aspect_ratio flag only has meaning for CEA-708.
if (isDigital) { if (isDigital) {
boolean isWideAspectRatio = (flags & 0x40) != 0; boolean isWideAspectRatio = (flags & 0x40) != 0;

View file

@ -15,13 +15,17 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.DtsUtil; import com.google.android.exoplayer2.audio.DtsUtil;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray; 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. * 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 static final int HEADER_SIZE = 18;
private final ParsableByteArray headerScratchBytes; private final ParsableByteArray headerScratchBytes;
private final String language; @Nullable private final String language;
private String formatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
private int state; private int state;
private int bytesRead; private int bytesRead;
@ -48,7 +52,7 @@ public final class DtsReader implements ElementaryStreamReader {
// Used when parsing the header. // Used when parsing the header.
private long sampleDurationUs; private long sampleDurationUs;
private Format format; @MonotonicNonNull private Format format;
private int sampleSize; private int sampleSize;
// Used when reading the samples. // Used when reading the samples.
@ -59,7 +63,7 @@ public final class DtsReader implements ElementaryStreamReader {
* *
* @param language Track language. * @param language Track language.
*/ */
public DtsReader(String language) { public DtsReader(@Nullable String language) {
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
state = STATE_FINDING_SYNC; state = STATE_FINDING_SYNC;
this.language = language; this.language = language;
@ -86,6 +90,7 @@ public final class DtsReader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SYNC: case STATE_FINDING_SYNC:
@ -162,9 +167,8 @@ public final class DtsReader implements ElementaryStreamReader {
return false; return false;
} }
/** /** Parses the sample header. */
* Parses the sample header. @RequiresNonNull("output")
*/
private void parseHeader() { private void parseHeader() {
byte[] frameData = headerScratchBytes.data; byte[] frameData = headerScratchBytes.data;
if (format == null) { if (format == null) {

View file

@ -64,12 +64,12 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
Format.createImageSampleFormat( Format.createImageSampleFormat(
idGenerator.getFormatId(), idGenerator.getFormatId(),
MimeTypes.APPLICATION_DVBSUBS, MimeTypes.APPLICATION_DVBSUBS,
null, /* codecs= */ null,
Format.NO_VALUE, Format.NO_VALUE,
0, /* selectionFlags= */ 0,
Collections.singletonList(subtitleInfo.initializationData), Collections.singletonList(subtitleInfo.initializationData),
subtitleInfo.language, subtitleInfo.language,
null)); /* drmInitData= */ null));
outputs[i] = output; outputs[i] = output;
} }
} }

View file

@ -16,16 +16,20 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Parses a continuous H262 byte stream and extracts individual frames. * 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_GROUP = 0xB8;
private static final int START_USER_DATA = 0xB2; private static final int START_USER_DATA = 0xB2;
private String formatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
// Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. // 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[] { private static final double[] FRAME_RATE_VALUES = new double[] {
24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; 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. // State that should not be reset on seek.
private boolean hasOutputFormat; private boolean hasOutputFormat;
private long frameDurationUs; 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. // Per packet state that gets reset at the start of each packet.
private long pesTimeUs; private long pesTimeUs;
@ -72,7 +76,7 @@ public final class H262Reader implements ElementaryStreamReader {
this(null); this(null);
} }
/* package */ H262Reader(UserDataReader userDataReader) { /* package */ H262Reader(@Nullable UserDataReader userDataReader) {
this.userDataReader = userDataReader; this.userDataReader = userDataReader;
prefixFlags = new boolean[4]; prefixFlags = new boolean[4];
csdBuffer = new CsdBuffer(128); csdBuffer = new CsdBuffer(128);
@ -89,7 +93,7 @@ public final class H262Reader implements ElementaryStreamReader {
public void seek() { public void seek() {
NalUnitUtil.clearPrefixFlags(prefixFlags); NalUnitUtil.clearPrefixFlags(prefixFlags);
csdBuffer.reset(); csdBuffer.reset();
if (userDataReader != null) { if (userData != null) {
userData.reset(); userData.reset();
} }
totalBytesWritten = 0; totalBytesWritten = 0;
@ -114,6 +118,7 @@ public final class H262Reader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
int offset = data.getPosition(); int offset = data.getPosition();
int limit = data.limit(); int limit = data.limit();
byte[] dataArray = data.data; byte[] dataArray = data.data;
@ -130,7 +135,7 @@ public final class H262Reader implements ElementaryStreamReader {
if (!hasOutputFormat) { if (!hasOutputFormat) {
csdBuffer.onData(dataArray, offset, limit); csdBuffer.onData(dataArray, offset, limit);
} }
if (userDataReader != null) { if (userData != null) {
userData.appendToNalUnit(dataArray, offset, limit); userData.appendToNalUnit(dataArray, offset, limit);
} }
return; return;
@ -157,7 +162,7 @@ public final class H262Reader implements ElementaryStreamReader {
hasOutputFormat = true; hasOutputFormat = true;
} }
} }
if (userDataReader != null) { if (userData != null) {
int bytesAlreadyPassed = 0; int bytesAlreadyPassed = 0;
if (lengthToStartCode > 0) { if (lengthToStartCode > 0) {
userData.appendToNalUnit(dataArray, offset, startCodeOffset); userData.appendToNalUnit(dataArray, offset, startCodeOffset);
@ -167,8 +172,8 @@ public final class H262Reader implements ElementaryStreamReader {
if (userData.endNalUnit(bytesAlreadyPassed)) { if (userData.endNalUnit(bytesAlreadyPassed)) {
int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength); int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength);
userDataParsable.reset(userData.nalData, unescapedLength); Util.castNonNull(userDataParsable).reset(userData.nalData, unescapedLength);
userDataReader.consume(sampleTimeUs, userDataParsable); Util.castNonNull(userDataReader).consume(sampleTimeUs, userDataParsable);
} }
if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) { 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 csdBuffer The csd buffer.
* @param formatId The id for the generated format. May be null. * @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 * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if
* 0 if the duration could not be determined. * 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); byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
int firstByte = csdData[4] & 0xFF; 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.NalUnitUtil.SpsData; import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; 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. * Parses a continuous H264 byte stream and extracts individual frames.
@ -51,9 +57,9 @@ public final class H264Reader implements ElementaryStreamReader {
private long totalBytesWritten; private long totalBytesWritten;
private final boolean[] prefixFlags; private final boolean[] prefixFlags;
private String formatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
private SampleReader sampleReader; @MonotonicNonNull private SampleReader sampleReader;
// State that should not be reset on seek. // State that should not be reset on seek.
private boolean hasOutputFormat; private boolean hasOutputFormat;
@ -87,13 +93,15 @@ public final class H264Reader implements ElementaryStreamReader {
@Override @Override
public void seek() { public void seek() {
totalBytesWritten = 0;
randomAccessIndicator = false;
NalUnitUtil.clearPrefixFlags(prefixFlags); NalUnitUtil.clearPrefixFlags(prefixFlags);
sps.reset(); sps.reset();
pps.reset(); pps.reset();
sei.reset(); sei.reset();
sampleReader.reset(); if (sampleReader != null) {
totalBytesWritten = 0; sampleReader.reset();
randomAccessIndicator = false; }
} }
@Override @Override
@ -113,6 +121,8 @@ public final class H264Reader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
assertTracksCreated();
int offset = data.getPosition(); int offset = data.getPosition();
int limit = data.limit(); int limit = data.limit();
byte[] dataArray = data.data; byte[] dataArray = data.data;
@ -159,6 +169,7 @@ public final class H264Reader implements ElementaryStreamReader {
// Do nothing. // Do nothing.
} }
@RequiresNonNull("sampleReader")
private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { private void startNalUnit(long position, int nalUnitType, long pesTimeUs) {
if (!hasOutputFormat || sampleReader.needsSpsPps()) { if (!hasOutputFormat || sampleReader.needsSpsPps()) {
sps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType);
@ -168,6 +179,7 @@ public final class H264Reader implements ElementaryStreamReader {
sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); sampleReader.startNalUnit(position, nalUnitType, pesTimeUs);
} }
@RequiresNonNull("sampleReader")
private void nalUnitData(byte[] dataArray, int offset, int limit) { private void nalUnitData(byte[] dataArray, int offset, int limit) {
if (!hasOutputFormat || sampleReader.needsSpsPps()) { if (!hasOutputFormat || sampleReader.needsSpsPps()) {
sps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit);
@ -177,6 +189,7 @@ public final class H264Reader implements ElementaryStreamReader {
sampleReader.appendToNalUnit(dataArray, offset, limit); sampleReader.appendToNalUnit(dataArray, offset, limit);
} }
@RequiresNonNull({"output", "sampleReader"})
private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
if (!hasOutputFormat || sampleReader.needsSpsPps()) { if (!hasOutputFormat || sampleReader.needsSpsPps()) {
sps.endNalUnit(discardPadding); 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. */ /** Consumes a stream of NAL units and outputs samples. */
private static final class SampleReader { private static final class SampleReader {
@ -478,7 +497,7 @@ public final class H264Reader implements ElementaryStreamReader {
private boolean isComplete; private boolean isComplete;
private boolean hasSliceType; private boolean hasSliceType;
private SpsData spsData; @Nullable private SpsData spsData;
private int nalRefIdc; private int nalRefIdc;
private int sliceType; private int sliceType;
private int frameNum; private int frameNum;
@ -542,6 +561,8 @@ public final class H264Reader implements ElementaryStreamReader {
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
// See ISO 14496-10 subsection 7.4.1.2.4. // See ISO 14496-10 subsection 7.4.1.2.4.
SpsData spsData = Assertions.checkStateNotNull(this.spsData);
SpsData otherSpsData = Assertions.checkStateNotNull(other.spsData);
return isComplete return isComplete
&& (!other.isComplete && (!other.isComplete
|| frameNum != other.frameNum || frameNum != other.frameNum
@ -552,15 +573,15 @@ public final class H264Reader implements ElementaryStreamReader {
&& bottomFieldFlag != other.bottomFieldFlag) && bottomFieldFlag != other.bottomFieldFlag)
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|| (spsData.picOrderCountType == 0 || (spsData.picOrderCountType == 0
&& other.spsData.picOrderCountType == 0 && otherSpsData.picOrderCountType == 0
&& (picOrderCntLsb != other.picOrderCntLsb && (picOrderCntLsb != other.picOrderCntLsb
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|| (spsData.picOrderCountType == 1 || (spsData.picOrderCountType == 1
&& other.spsData.picOrderCountType == 1 && otherSpsData.picOrderCountType == 1
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) || deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|| idrPicFlag != other.idrPicFlag || 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray; import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
import com.google.android.exoplayer2.util.Util;
import java.util.Collections; 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. * 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 final SeiReader seiReader;
private String formatId; @MonotonicNonNull private String formatId;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
private SampleReader sampleReader; @MonotonicNonNull private SampleReader sampleReader;
// State that should not be reset on seek. // State that should not be reset on seek.
private boolean hasOutputFormat; private boolean hasOutputFormat;
@ -84,14 +90,16 @@ public final class H265Reader implements ElementaryStreamReader {
@Override @Override
public void seek() { public void seek() {
totalBytesWritten = 0;
NalUnitUtil.clearPrefixFlags(prefixFlags); NalUnitUtil.clearPrefixFlags(prefixFlags);
vps.reset(); vps.reset();
sps.reset(); sps.reset();
pps.reset(); pps.reset();
prefixSei.reset(); prefixSei.reset();
suffixSei.reset(); suffixSei.reset();
sampleReader.reset(); if (sampleReader != null) {
totalBytesWritten = 0; sampleReader.reset();
}
} }
@Override @Override
@ -111,6 +119,8 @@ public final class H265Reader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
assertTracksCreated();
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
int offset = data.getPosition(); int offset = data.getPosition();
int limit = data.limit(); int limit = data.limit();
@ -160,6 +170,7 @@ public final class H265Reader implements ElementaryStreamReader {
// Do nothing. // Do nothing.
} }
@RequiresNonNull("sampleReader")
private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
if (hasOutputFormat) { if (hasOutputFormat) {
sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);
@ -172,6 +183,7 @@ public final class H265Reader implements ElementaryStreamReader {
suffixSei.startNalUnit(nalUnitType); suffixSei.startNalUnit(nalUnitType);
} }
@RequiresNonNull("sampleReader")
private void nalUnitData(byte[] dataArray, int offset, int limit) { private void nalUnitData(byte[] dataArray, int offset, int limit) {
if (hasOutputFormat) { if (hasOutputFormat) {
sampleReader.readNalUnitData(dataArray, offset, limit); sampleReader.readNalUnitData(dataArray, offset, limit);
@ -184,6 +196,7 @@ public final class H265Reader implements ElementaryStreamReader {
suffixSei.appendToNalUnit(dataArray, offset, limit); suffixSei.appendToNalUnit(dataArray, offset, limit);
} }
@RequiresNonNull({"output", "sampleReader"})
private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
if (hasOutputFormat) { if (hasOutputFormat) {
sampleReader.endNalUnit(position, offset); sampleReader.endNalUnit(position, offset);
@ -214,8 +227,11 @@ public final class H265Reader implements ElementaryStreamReader {
} }
} }
private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, private static Format parseMediaFormat(
NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { @Nullable String formatId,
NalUnitTargetBuffer vps,
NalUnitTargetBuffer sps,
NalUnitTargetBuffer pps) {
// Build codec-specific data. // Build codec-specific data.
byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
System.arraycopy(vps.nalData, 0, csd, 0, vps.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 { 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Parses ID3 data and extracts individual text information frames. * Parses ID3 data and extracts individual text information frames.
@ -36,7 +38,7 @@ public final class Id3Reader implements ElementaryStreamReader {
private final ParsableByteArray id3Header; private final ParsableByteArray id3Header;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
// State that should be reset on seek. // State that should be reset on seek.
private boolean writingSample; private boolean writingSample;
@ -76,6 +78,7 @@ public final class Id3Reader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
if (!writingSample) { if (!writingSample) {
return; return;
} }
@ -106,6 +109,7 @@ public final class Id3Reader implements ElementaryStreamReader {
@Override @Override
public void packetFinished() { public void packetFinished() {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {
return; 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.Collections; 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. * 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_FIRST = 0x56;
private static final int SYNC_BYTE_SECOND = 0xE0; private static final int SYNC_BYTE_SECOND = 0xE0;
private final String language; @Nullable private final String language;
private final ParsableByteArray sampleDataBuffer; private final ParsableByteArray sampleDataBuffer;
private final ParsableBitArray sampleBitArray; private final ParsableBitArray sampleBitArray;
// Track output info. // Track output info.
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
private Format format; @MonotonicNonNull private String formatId;
private String formatId; @MonotonicNonNull private Format format;
// Parser state info. // Parser state info.
private int state; private int state;
@ -99,6 +102,7 @@ public final class LatmReader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) throws ParserException { public void consume(ParsableByteArray data) throws ParserException {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
int bytesToRead; int bytesToRead;
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
@ -150,6 +154,7 @@ public final class LatmReader implements ElementaryStreamReader {
* *
* @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes.
*/ */
@RequiresNonNull("output")
private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { private void parseAudioMuxElement(ParsableBitArray data) throws ParserException {
boolean useSameStreamMux = data.readBit(); boolean useSameStreamMux = data.readBit();
if (!useSameStreamMux) { 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 { private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException {
int audioMuxVersion = data.readBits(1); int audioMuxVersion = data.readBits(1);
audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;
@ -198,9 +202,19 @@ public final class LatmReader implements ElementaryStreamReader {
data.setPosition(startPosition); data.setPosition(startPosition);
byte[] initData = new byte[(readBits + 7) / 8]; byte[] initData = new byte[(readBits + 7) / 8];
data.readBits(initData, 0, readBits); data.readBits(initData, 0, readBits);
Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, Format format =
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, Format.createAudioSampleFormat(
Collections.singletonList(initData), null, 0, language); 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)) { if (!format.equals(this.format)) {
this.format = format; this.format = format;
sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; 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) { private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) {
// The start of sample data in // The start of sample data in
int bitPosition = data.getPosition(); 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.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray; 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. * 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 ParsableByteArray headerScratch;
private final MpegAudioHeader header; private final MpegAudioHeader header;
private final String language; @Nullable private final String language;
private String formatId; @MonotonicNonNull private TrackOutput output;
private TrackOutput output; @MonotonicNonNull private String formatId;
private int state; private int state;
private int frameBytesRead; private int frameBytesRead;
@ -59,7 +63,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
this(null); this(null);
} }
public MpegAudioReader(String language) { public MpegAudioReader(@Nullable String language) {
state = STATE_FINDING_HEADER; state = STATE_FINDING_HEADER;
// The first byte of an MPEG Audio frame header is always 0xFF. // The first byte of an MPEG Audio frame header is always 0xFF.
headerScratch = new ParsableByteArray(4); headerScratch = new ParsableByteArray(4);
@ -89,6 +93,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) {
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: 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. * 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 * 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 * output as sample data, and the position of the source is advanced to the byte that immediately
* follows the header. * follows the header.
* <p> *
* If a frame header is read in full but cannot be parsed then the state is changed to * <p>If a frame header is read in full but cannot be parsed then the state is changed to {@link
* {@link #STATE_READING_HEADER}. * #STATE_READING_HEADER}.
* <p> *
* If a frame header is not read in full then the position of the source is advanced to the limit, * <p>If a frame header is not read in full then the position of the source is advanced to the
* and the method should be called again with the next source to continue the read. * limit, and the method should be called again with the next source to continue the read.
* *
* @param source The source from which to read. * @param source The source from which to read.
*/ */
@RequiresNonNull("output")
private void readHeaderRemainder(ParsableByteArray source) { private void readHeaderRemainder(ParsableByteArray source) {
int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead);
source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); source.readBytes(headerScratch.data, frameBytesRead, bytesToRead);
@ -195,16 +201,17 @@ public final class MpegAudioReader implements ElementaryStreamReader {
/** /**
* Attempts to read the remainder of the frame. * 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 * position of the source will have been advanced to the byte that immediately follows the end of
* the frame. * the frame.
* <p> *
* If a frame is not read in full then the position of the source will have been advanced to the * <p>If a frame is not read in full then the position of the source will have been advanced to
* limit, and the method should be called again with the next source to continue the read. * 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. * @param source The source from which to read.
*/ */
@RequiresNonNull("output")
private void readFrameRemainder(ParsableByteArray source) { private void readFrameRemainder(ParsableByteArray source) {
int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead);
output.sampleData(source, bytesToRead); output.sampleData(source, bytesToRead);
@ -219,5 +226,4 @@ public final class MpegAudioReader implements ElementaryStreamReader {
frameBytesRead = 0; frameBytesRead = 0;
state = STATE_FINDING_HEADER; state = STATE_FINDING_HEADER;
} }
} }

View file

@ -15,13 +15,17 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.Log;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; 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. * Parses PES packet data and extracts samples.
@ -45,7 +49,7 @@ public final class PesReader implements TsPayloadReader {
private int state; private int state;
private int bytesRead; private int bytesRead;
private TimestampAdjuster timestampAdjuster; @MonotonicNonNull private TimestampAdjuster timestampAdjuster;
private boolean ptsFlag; private boolean ptsFlag;
private boolean dtsFlag; private boolean dtsFlag;
private boolean seenFirstDts; private boolean seenFirstDts;
@ -79,6 +83,8 @@ public final class PesReader implements TsPayloadReader {
@Override @Override
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { 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) { if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:
@ -119,7 +125,7 @@ public final class PesReader implements TsPayloadReader {
int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); 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. // Read as much of the extended header as we're interested in, and skip the rest.
if (continueRead(data, pesScratch.data, readLength) if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, null, extendedHeaderLength)) { && continueRead(data, /* target= */ null, extendedHeaderLength)) {
parseHeaderExtension(); parseHeaderExtension();
flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
reader.packetStarted(timeUs, flags); reader.packetStarted(timeUs, flags);
@ -162,7 +168,8 @@ public final class PesReader implements TsPayloadReader {
* @param targetLength The target length of the read. * @param targetLength The target length of the read.
* @return Whether the target length has been reached. * @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); int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
if (bytesToRead <= 0) { if (bytesToRead <= 0) {
return true; return true;
@ -207,6 +214,7 @@ public final class PesReader implements TsPayloadReader {
return true; return true;
} }
@RequiresNonNull("timestampAdjuster")
private void parseHeaderExtension() { private void parseHeaderExtension() {
pesScratch.setPosition(0); pesScratch.setPosition(0);
timeUs = C.TIME_UNSET; timeUs = C.TIME_UNSET;

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor; 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.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; 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.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException; 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. * Extracts data from the MPEG-2 PS container format.
@ -67,8 +71,8 @@ public final class PsExtractor implements Extractor {
private long lastTrackPosition; private long lastTrackPosition;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private PsBinarySearchSeeker psBinarySearchSeeker; @Nullable private PsBinarySearchSeeker psBinarySearchSeeker;
private ExtractorOutput output; @MonotonicNonNull private ExtractorOutput output;
private boolean hasOutputSeekMap; private boolean hasOutputSeekMap;
public PsExtractor() { public PsExtractor() {
@ -160,6 +164,7 @@ public final class PsExtractor implements Extractor {
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
Assertions.checkStateNotNull(output); // Asserts init has been called.
long inputLength = input.getLength(); long inputLength = input.getLength();
boolean canReadDuration = inputLength != C.LENGTH_UNSET; boolean canReadDuration = inputLength != C.LENGTH_UNSET;
@ -221,7 +226,7 @@ public final class PsExtractor implements Extractor {
PesReader payloadReader = psPayloadReaders.get(streamId); PesReader payloadReader = psPayloadReaders.get(streamId);
if (!foundAllTracks) { if (!foundAllTracks) {
if (payloadReader == null) { if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null; @Nullable ElementaryStreamReader elementaryStreamReader = null;
if (streamId == PRIVATE_STREAM_1) { if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio. // Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only // 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. // Internals.
@RequiresNonNull("output")
private void maybeOutputSeekMap(long inputLength) { private void maybeOutputSeekMap(long inputLength) {
if (!hasOutputSeekMap) { if (!hasOutputSeekMap) {
hasOutputSeekMap = true; hasOutputSeekMap = true;

View file

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -45,7 +46,7 @@ public final class SeiReader {
idGenerator.generateNewId(); idGenerator.generateNewId();
TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
Format channelFormat = closedCaptionFormats.get(i); Format channelFormat = closedCaptionFormats.get(i);
String channelMimeType = channelFormat.sampleMimeType; @Nullable String channelMimeType = channelFormat.sampleMimeType;
Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
|| MimeTypes.APPLICATION_CEA708.equals(channelMimeType), || MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
"Invalid closed caption mime type provided: " + channelMimeType); "Invalid closed caption mime type provided: " + channelMimeType);
@ -69,5 +70,4 @@ public final class SeiReader {
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
CeaUtil.consume(pesTimeUs, seiBuffer, outputs); 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.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; 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.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; 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. * Parses splice info sections as defined by SCTE35.
*/ */
public final class SpliceInfoSectionReader implements SectionPayloadReader { public final class SpliceInfoSectionReader implements SectionPayloadReader {
private TimestampAdjuster timestampAdjuster; @MonotonicNonNull private TimestampAdjuster timestampAdjuster;
private TrackOutput output; @MonotonicNonNull private TrackOutput output;
private boolean formatDeclared; private boolean formatDeclared;
@Override @Override
@ -44,6 +48,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
@Override @Override
public void consume(ParsableByteArray sectionData) { public void consume(ParsableByteArray sectionData) {
assertInitialized();
if (!formatDeclared) { if (!formatDeclared) {
if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) { if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
// There is not enough information to initialize the timestamp adjuster. // There is not enough information to initialize the timestamp adjuster.
@ -59,4 +64,9 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
sampleSize, 0, null); 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.SparseBooleanArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
@ -587,8 +588,11 @@ public final class TsExtractor implements Extractor {
continue; continue;
} }
TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader @Nullable
: payloadReaderFactory.createPayloadReader(streamType, esInfo); TsPayloadReader reader =
mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3
? id3Reader
: payloadReaderFactory.createPayloadReader(streamType, esInfo);
if (mode != MODE_HLS if (mode != MODE_HLS
|| elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {
trackIdToPidScratch.put(trackId, elementaryPid); trackIdToPidScratch.put(trackId, elementaryPid);
@ -602,7 +606,7 @@ public final class TsExtractor implements Extractor {
int trackPid = trackIdToPidScratch.valueAt(i); int trackPid = trackIdToPidScratch.valueAt(i);
trackIds.put(trackId, true); trackIds.put(trackId, true);
trackPids.put(trackPid, true); trackPids.put(trackPid, true);
TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); @Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
if (reader != null) { if (reader != null) {
if (reader != id3Reader) { if (reader != id3Reader) {
reader.init(timestampAdjuster, output, reader.init(timestampAdjuster, output,

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; 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 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. * @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. * {@code null} if the stream is not supported.
*/ */
@Nullable
TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo); TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo);
} }
/** /**
@ -66,18 +67,21 @@ public interface TsPayloadReader {
final class EsInfo { final class EsInfo {
public final int streamType; public final int streamType;
public final String language; @Nullable public final String language;
public final List<DvbSubtitleInfo> dvbSubtitleInfos; public final List<DvbSubtitleInfo> dvbSubtitleInfos;
public final byte[] descriptorBytes; public final byte[] descriptorBytes;
/** /**
* @param streamType The type of the stream as defined by the * @param streamType The type of the stream as defined by the {@link TsExtractor}{@code
* {@link TsExtractor}{@code .TS_STREAM_TYPE_*}. * .TS_STREAM_TYPE_*}.
* @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18. * @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 dvbSubtitleInfos Information about DVB subtitles associated to the stream.
* @param descriptorBytes The descriptor bytes 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) { byte[] descriptorBytes) {
this.streamType = streamType; this.streamType = streamType;
this.language = language; this.language = language;
@ -134,6 +138,7 @@ public interface TsPayloadReader {
this.firstTrackId = firstTrackId; this.firstTrackId = firstTrackId;
this.trackIdIncrement = trackIdIncrement; this.trackIdIncrement = trackIdIncrement;
trackId = ID_UNSET; trackId = ID_UNSET;
formatId = "";
} }
/** /**

View file

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -44,7 +45,7 @@ import java.util.List;
idGenerator.generateNewId(); idGenerator.generateNewId();
TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
Format channelFormat = closedCaptionFormats.get(i); Format channelFormat = closedCaptionFormats.get(i);
String channelMimeType = channelFormat.sampleMimeType; @Nullable String channelMimeType = channelFormat.sampleMimeType;
Assertions.checkArgument( Assertions.checkArgument(
MimeTypes.APPLICATION_CEA608.equals(channelMimeType) MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
|| MimeTypes.APPLICATION_CEA708.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.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Extracts data from WAV byte streams. * Extracts data from WAV byte streams.
@ -46,9 +49,9 @@ public final class WavExtractor implements Extractor {
/** Factory for {@link WavExtractor} instances. */ /** Factory for {@link WavExtractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
private ExtractorOutput extractorOutput; @MonotonicNonNull private ExtractorOutput extractorOutput;
private TrackOutput trackOutput; @MonotonicNonNull private TrackOutput trackOutput;
private OutputWriter outputWriter; @MonotonicNonNull private OutputWriter outputWriter;
private int dataStartPosition; private int dataStartPosition;
private long dataEndPosition; private long dataEndPosition;
@ -84,6 +87,7 @@ public final class WavExtractor implements Extractor {
@Override @Override
public int read(ExtractorInput input, PositionHolder seekPosition) public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
assertInitialized();
if (outputWriter == null) { if (outputWriter == null) {
WavHeader header = WavHeaderReader.peek(input); WavHeader header = WavHeaderReader.peek(input);
if (header == null) { if (header == null) {
@ -91,12 +95,34 @@ public final class WavExtractor implements Extractor {
throw new ParserException("Unsupported or unrecognized wav header."); throw new ParserException("Unsupported or unrecognized wav header.");
} }
@C.PcmEncoding if (header.formatType == WavUtil.TYPE_IMA_ADPCM) {
int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header);
if (pcmEncoding == C.ENCODING_INVALID) { } else if (header.formatType == WavUtil.TYPE_ALAW) {
throw new ParserException("Unsupported WAV format type: " + header.formatType); 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) { 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; 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. */ /** Writes to the extractor's output. */
private interface OutputWriter { private interface OutputWriter {
@ -150,61 +182,56 @@ public final class WavExtractor implements Extractor {
throws IOException, InterruptedException; throws IOException, InterruptedException;
} }
private static final class PcmOutputWriter implements OutputWriter { private static final class PassthroughOutputWriter implements OutputWriter {
private final ExtractorOutput extractorOutput; private final ExtractorOutput extractorOutput;
private final TrackOutput trackOutput; private final TrackOutput trackOutput;
private final WavHeader header; private final WavHeader header;
private final @C.PcmEncoding int pcmEncoding; private final Format format;
private final int targetSampleSize; /** 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; 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 long outputFrameCount;
private int pendingBytes;
public PcmOutputWriter( public PassthroughOutputWriter(
ExtractorOutput extractorOutput, ExtractorOutput extractorOutput,
TrackOutput trackOutput, TrackOutput trackOutput,
WavHeader header, WavHeader header,
@C.PcmEncoding int pcmEncoding) { String mimeType,
@C.PcmEncoding int pcmEncoding)
throws ParserException {
this.extractorOutput = extractorOutput; this.extractorOutput = extractorOutput;
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
this.header = header; 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; int bytesPerFrame = header.numChannels * header.bitsPerSample / 8;
// Validate the header. Blocks are expected to correspond to single frames.
if (header.blockSize != bytesPerFrame) { if (header.blockSize != bytesPerFrame) {
throw new ParserException( throw new ParserException(
"Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize);
} }
// Output the seek map. targetSampleSizeBytes =
extractorOutput.seekMap( Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); format =
// Output the format.
Format format =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
/* id= */ null, /* id= */ null,
MimeTypes.AUDIO_RAW, mimeType,
/* codecs= */ null, /* codecs= */ null,
/* bitrate= */ header.averageBytesPerSecond * 8, /* bitrate= */ header.frameRateHz * bytesPerFrame * 8,
targetSampleSize, /* maxInputSize= */ targetSampleSizeBytes,
header.numChannels, header.numChannels,
header.frameRateHz, header.frameRateHz,
pcmEncoding, pcmEncoding,
@ -212,6 +239,19 @@ public final class WavExtractor implements Extractor {
/* drmInitData= */ null, /* drmInitData= */ null,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* language= */ null); /* 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); trackOutput.format(format);
} }
@ -220,34 +260,303 @@ public final class WavExtractor implements Extractor {
throws IOException, InterruptedException { throws IOException, InterruptedException {
// Write sample data until we've reached the target sample size, or the end of the data. // Write sample data until we've reached the target sample size, or the end of the data.
boolean endOfSampleData = bytesLeft == 0; boolean endOfSampleData = bytesLeft == 0;
while (!endOfSampleData && pendingBytes < targetSampleSize) { while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) {
int bytesToRead = (int) Math.min(targetSampleSize - pendingBytes, bytesLeft); int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft);
int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true);
if (bytesAppended == RESULT_END_OF_INPUT) { if (bytesAppended == RESULT_END_OF_INPUT) {
endOfSampleData = true; endOfSampleData = true;
} else { } else {
pendingBytes += bytesAppended; pendingOutputBytes += bytesAppended;
} }
} }
// Write the corresponding sample metadata. Samples must be a whole number of frames. It's // 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 bytesPerFrame = header.blockSize;
int pendingFrames = pendingBytes / bytesPerFrame; int pendingFrames = pendingOutputBytes / bytesPerFrame;
if (pendingFrames > 0) { if (pendingFrames > 0) {
long timeUs = long timeUs =
startTimeUs startTimeUs
+ Util.scaleLargeTimestamp( + Util.scaleLargeTimestamp(
outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
int size = pendingFrames * bytesPerFrame; int size = pendingFrames * bytesPerFrame;
int offset = pendingBytes - size; int offset = pendingOutputBytes - size;
trackOutput.sampleMetadata( trackOutput.sampleMetadata(
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null);
outputFrameCount += pendingFrames; outputFrameCount += pendingFrames;
pendingBytes = offset; pendingOutputBytes = offset;
} }
return endOfSampleData; 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 @Override
public SeekPoints getSeekPoints(long timeUs) { 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. // Calculate the containing block index, constraining to valid indices.
long blockSize = wavHeader.blockSize; long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock);
long blockIndex = Util.constrainValue(positionOffset / blockSize, 0, blockCount - 1); blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1);
long seekPosition = firstBlockPosition + (blockIndex * blockSize); long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize);
long seekTimeUs = blockIndexToTimeUs(blockIndex); long seekTimeUs = blockIndexToTimeUs(blockIndex);
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) {
return new SeekPoints(seekPoint); return new SeekPoints(seekPoint);
} else { } else {
long secondBlockIndex = blockIndex + 1; long secondBlockIndex = blockIndex + 1;
long secondSeekPosition = firstBlockPosition + (secondBlockIndex * blockSize); long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize);
long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex);
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint); return new SeekPoints(seekPoint, secondSeekPoint);

View file

@ -33,12 +33,12 @@ import com.google.android.exoplayer2.util.Assertions;
*/ */
@RequiresApi(21) @RequiresApi(21)
/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter { /* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
private MediaCodecAsyncCallback mediaCodecAsyncCallback; private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
private final Handler handler; private final Handler handler;
private final MediaCodec codec; private final MediaCodec codec;
@Nullable private IllegalStateException internalException; @Nullable private IllegalStateException internalException;
private boolean flushing; private boolean flushing;
private Runnable onCodecStart; private Runnable codecStartRunnable;
/** /**
* Create a new {@code AsynchronousMediaCodecAdapter}. * Create a new {@code AsynchronousMediaCodecAdapter}.
@ -51,11 +51,16 @@ import com.google.android.exoplayer2.util.Assertions;
@VisibleForTesting @VisibleForTesting
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) { /* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) {
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback(); mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
handler = new Handler(looper); handler = new Handler(looper);
this.codec = codec; this.codec = codec;
this.codec.setCallback(mediaCodecAsyncCallback); this.codec.setCallback(mediaCodecAsyncCallback);
onCodecStart = () -> codec.start(); codecStartRunnable = codec::start;
}
@Override
public void start() {
codecStartRunnable.run();
} }
@Override @Override
@ -105,7 +110,7 @@ import com.google.android.exoplayer2.util.Assertions;
flushing = false; flushing = false;
mediaCodecAsyncCallback.flush(); mediaCodecAsyncCallback.flush();
try { try {
onCodecStart.run(); codecStartRunnable.run();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// Catch IllegalStateException directly so that we don't have to wrap it. // Catch IllegalStateException directly so that we don't have to wrap it.
internalException = e; internalException = e;
@ -115,8 +120,8 @@ import com.google.android.exoplayer2.util.Assertions;
} }
@VisibleForTesting @VisibleForTesting
/* package */ void setOnCodecStart(Runnable onCodecStart) { /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
this.onCodecStart = onCodecStart; this.codecStartRunnable = codecStartRunnable;
} }
private void maybeThrowException() throws IllegalStateException { private void maybeThrowException() throws IllegalStateException {

View file

@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -54,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@MonotonicNonNull private Handler handler; @MonotonicNonNull private Handler handler;
private long pendingFlushCount; private long pendingFlushCount;
private @State int state; private @State int state;
private Runnable onCodecStart; private Runnable codecStartRunnable;
@Nullable private IllegalStateException internalException; @Nullable private IllegalStateException internalException;
/** /**
@ -77,31 +76,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.codec = codec; this.codec = codec;
this.handlerThread = handlerThread; this.handlerThread = handlerThread;
state = STATE_CREATED; state = STATE_CREATED;
onCodecStart = codec::start; codecStartRunnable = codec::start;
} }
/** @Override
* 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.
*/
public synchronized void start() { public synchronized void start() {
Assertions.checkState(state == STATE_CREATED);
handlerThread.start(); handlerThread.start();
handler = new Handler(handlerThread.getLooper()); handler = new Handler(handlerThread.getLooper());
codec.setCallback(this, handler); codec.setCallback(this, handler);
codecStartRunnable.run();
state = STATE_STARTED; state = STATE_STARTED;
} }
@Override @Override
public synchronized int dequeueInputBufferIndex() { public synchronized int dequeueInputBufferIndex() {
Assertions.checkState(state == STATE_STARTED);
if (isFlushing()) { if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} else { } else {
@ -112,8 +100,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
Assertions.checkState(state == STATE_STARTED);
if (isFlushing()) { if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} else { } else {
@ -124,15 +110,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public synchronized MediaFormat getOutputFormat() { public synchronized MediaFormat getOutputFormat() {
Assertions.checkState(state == STATE_STARTED);
return mediaCodecAsyncCallback.getOutputFormat(); return mediaCodecAsyncCallback.getOutputFormat();
} }
@Override @Override
public synchronized void flush() { public synchronized void flush() {
Assertions.checkState(state == STATE_STARTED);
codec.flush(); codec.flush();
++pendingFlushCount; ++pendingFlushCount;
Util.castNonNull(handler).post(this::onFlushCompleted); Util.castNonNull(handler).post(this::onFlushCompleted);
@ -177,8 +159,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@VisibleForTesting @VisibleForTesting
/* package */ void setOnCodecStart(Runnable onCodecStart) { /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
this.onCodecStart = onCodecStart; this.codecStartRunnable = codecStartRunnable;
} }
private synchronized void onFlushCompleted() { private synchronized void onFlushCompleted() {
@ -199,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
mediaCodecAsyncCallback.flush(); mediaCodecAsyncCallback.flush();
try { try {
onCodecStart.run(); codecStartRunnable.run();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
internalException = e; internalException = e;
} catch (Exception e) { } catch (Exception e) {

View file

@ -31,6 +31,13 @@ import android.media.MediaFormat;
*/ */
/* package */ interface MediaCodecAdapter { /* 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 * Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link
* MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists. * 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 } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD
&& Util.SDK_INT >= 23) { && Util.SDK_INT >= 23) {
codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType()); codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType());
((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start();
} else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK } else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK
&& Util.SDK_INT >= 23) { && Util.SDK_INT >= 23) {
codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType()); codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType());
((MultiLockAsyncMediaCodecAdapter) codecAdapter).start();
} else { } else {
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs()); codecAdapter = new SynchronousMediaCodecAdapter(codec);
} }
TraceUtil.endSection(); TraceUtil.endSection();
@ -1009,7 +1007,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate); configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate);
TraceUtil.endSection(); TraceUtil.endSection();
TraceUtil.beginSection("startCodec"); TraceUtil.beginSection("startCodec");
codec.start(); codecAdapter.start();
TraceUtil.endSection(); TraceUtil.endSection();
codecInitializedTimestamp = SystemClock.elapsedRealtime(); codecInitializedTimestamp = SystemClock.elapsedRealtime();
getCodecBuffers(codec); getCodecBuffers(codec);
@ -1460,15 +1458,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& SystemClock.elapsedRealtime() < codecHotswapDeadlineMs)); && 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, * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate,
* current {@link Format} and set of possible stream formats. * 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.RequiresApi;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; 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.IntArrayQueue;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque; import java.util.ArrayDeque;
@ -94,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final HandlerThread handlerThread; private final HandlerThread handlerThread;
@MonotonicNonNull private Handler handler; @MonotonicNonNull private Handler handler;
private Runnable onCodecStart; private Runnable codecStartRunnable;
/** Creates a new instance that wraps the specified {@link MediaCodec}. */ /** Creates a new instance that wraps the specified {@link MediaCodec}. */
/* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) { /* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) {
@ -114,25 +113,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
codecException = null; codecException = null;
state = STATE_CREATED; state = STATE_CREATED;
this.handlerThread = handlerThread; this.handlerThread = handlerThread;
onCodecStart = codec::start; codecStartRunnable = codec::start;
} }
/** @Override
* 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.
*/
public void start() { public void start() {
synchronized (objectStateLock) { synchronized (objectStateLock) {
Assertions.checkState(state == STATE_CREATED);
handlerThread.start(); handlerThread.start();
handler = new Handler(handlerThread.getLooper()); handler = new Handler(handlerThread.getLooper());
codec.setCallback(this, handler); codec.setCallback(this, handler);
codecStartRunnable.run();
state = STATE_STARTED; state = STATE_STARTED;
} }
} }
@ -140,8 +130,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public int dequeueInputBufferIndex() { public int dequeueInputBufferIndex() {
synchronized (objectStateLock) { synchronized (objectStateLock) {
Assertions.checkState(state == STATE_STARTED);
if (isFlushing()) { if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} else { } else {
@ -154,8 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
synchronized (objectStateLock) { synchronized (objectStateLock) {
Assertions.checkState(state == STATE_STARTED);
if (isFlushing()) { if (isFlushing()) {
return MediaCodec.INFO_TRY_AGAIN_LATER; return MediaCodec.INFO_TRY_AGAIN_LATER;
} else { } else {
@ -168,8 +154,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public MediaFormat getOutputFormat() { public MediaFormat getOutputFormat() {
synchronized (objectStateLock) { synchronized (objectStateLock) {
Assertions.checkState(state == STATE_STARTED);
if (currentFormat == null) { if (currentFormat == null) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
@ -181,8 +165,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public void flush() { public void flush() {
synchronized (objectStateLock) { synchronized (objectStateLock) {
Assertions.checkState(state == STATE_STARTED);
codec.flush(); codec.flush();
pendingFlush++; pendingFlush++;
Util.castNonNull(handler).post(this::onFlushComplete); Util.castNonNull(handler).post(this::onFlushComplete);
@ -200,8 +182,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@VisibleForTesting @VisibleForTesting
/* package */ void setOnCodecStart(Runnable onCodecStart) { /* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
this.onCodecStart = onCodecStart; this.codecStartRunnable = codecStartRunnable;
} }
private int dequeueAvailableInputBufferIndex() { private int dequeueAvailableInputBufferIndex() {
@ -307,7 +289,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
clearAvailableOutput(); clearAvailableOutput();
codecException = null; codecException = null;
try { try {
onCodecStart.run(); codecStartRunnable.run();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
codecException = e; codecException = e;
} catch (Exception 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. * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode.
*/ */
/* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter { /* 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.codec = mediaCodec;
this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs; }
@Override
public void start() {
codec.start();
} }
@Override @Override
@ -38,7 +42,7 @@ import android.media.MediaFormat;
@Override @Override
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs); return codec.dequeueOutputBuffer(bufferInfo, 0);
} }
@Override @Override

View file

@ -773,7 +773,7 @@ public final class DownloadHelper {
} }
// Initialization of array of Lists. // Initialization of array of Lists.
@SuppressWarnings("unchecked") @SuppressWarnings({"unchecked", "rawtypes"})
private void onMediaPrepared() { private void onMediaPrepared() {
Assertions.checkNotNull(mediaPreparer); Assertions.checkNotNull(mediaPreparer);
Assertions.checkNotNull(mediaPreparer.mediaPeriods); Assertions.checkNotNull(mediaPreparer.mediaPeriods);

View file

@ -165,7 +165,7 @@ public final class Requirements implements Parcelable {
private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) {
// It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only // 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. // 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) { if (Util.SDK_INT < 24) {
return true; 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. * @return The parsed object data.
*/ */
// incompatible types in argument.
@SuppressWarnings("nullness:argument.type.incompatible")
private static ObjectData parseObjectData(ParsableBitArray data) { private static ObjectData parseObjectData(ParsableBitArray data) {
int objectId = data.readBits(16); int objectId = data.readBits(16);
data.skipBits(4); // Skip object_version_number data.skipBits(4); // Skip object_version_number
@ -490,8 +488,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
boolean nonModifyingColorFlag = data.readBit(); boolean nonModifyingColorFlag = data.readBit();
data.skipBits(1); // Skip reserved. data.skipBits(1); // Skip reserved.
@Nullable byte[] topFieldData = null; byte[] topFieldData = Util.EMPTY_BYTE_ARRAY;
@Nullable byte[] bottomFieldData = null; byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY;
if (objectCodingMethod == OBJECT_CODING_STRING) { if (objectCodingMethod == OBJECT_CODING_STRING) {
int numberOfCodes = data.readBits(8); 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; package com.google.android.exoplayer2.text.ttml;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan; import android.text.style.AbsoluteSizeSpan;
@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import com.google.android.exoplayer2.text.SpanUtil;
import java.util.Map; import java.util.Map;
/** /**
@ -77,32 +77,60 @@ import java.util.Map;
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasFontColor()) { if (style.hasFontColor()) {
builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, SpanUtil.addOrReplaceSpan(
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); builder,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasBackgroundColor()) { if (style.hasBackgroundColor()) {
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, SpanUtil.addOrReplaceSpan(
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); builder,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getFontFamily() != null) { 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getTextAlign() != null) { 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
switch (style.getFontSizeUnit()) { switch (style.getFontSizeUnit()) {
case TtmlStyle.FONT_SIZE_UNIT_PIXEL: 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case TtmlStyle.FONT_SIZE_UNIT_EM: 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case TtmlStyle.FONT_SIZE_UNIT_PERCENT: 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case TtmlStyle.UNSPECIFIED: case TtmlStyle.UNSPECIFIED:

View file

@ -31,14 +31,19 @@ import java.util.regex.Pattern;
*/ */
/* package */ final class CssParser { /* 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_BGCOLOR = "background-color";
private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_FAMILY = "font-family";
private static final String PROPERTY_FONT_WEIGHT = "font-weight"; 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 PROPERTY_TEXT_DECORATION = "text-decoration";
private static final String VALUE_BOLD = "bold"; private static final String VALUE_BOLD = "bold";
private static final String VALUE_UNDERLINE = "underline"; 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 PROPERTY_FONT_STYLE = "font-style";
private static final String VALUE_ITALIC = "italic"; private static final String VALUE_ITALIC = "italic";
@ -182,6 +187,8 @@ import java.util.regex.Pattern;
style.setFontColor(ColorParser.parseCssColor(value)); style.setFontColor(ColorParser.parseCssColor(value));
} else if (PROPERTY_BGCOLOR.equals(property)) { } else if (PROPERTY_BGCOLOR.equals(property)) {
style.setBackgroundColor(ColorParser.parseCssColor(value)); 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)) { } else if (PROPERTY_TEXT_DECORATION.equals(property)) {
if (VALUE_UNDERLINE.equals(value)) { if (VALUE_UNDERLINE.equals(value)) {
style.setUnderline(true); style.setUnderline(true);

View file

@ -95,6 +95,7 @@ public final class WebvttCssStyle {
@FontSizeUnit private int fontSizeUnit; @FontSizeUnit private int fontSizeUnit;
private float fontSize; private float fontSize;
@Nullable private Layout.Alignment textAlign; @Nullable private Layout.Alignment textAlign;
private boolean combineUpright;
// Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed
// because reset() only assigns fields, it doesn't read any. // because reset() only assigns fields, it doesn't read any.
@ -118,6 +119,7 @@ public final class WebvttCssStyle {
italic = UNSPECIFIED; italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED; fontSizeUnit = UNSPECIFIED;
textAlign = null; textAlign = null;
combineUpright = false;
} }
public void setTargetId(String targetId) { public void setTargetId(String targetId) {
@ -287,35 +289,12 @@ public final class WebvttCssStyle {
return fontSize; return fontSize;
} }
public void cascadeFrom(WebvttCssStyle style) { public void setCombineUpright(boolean enabled) {
if (style.hasFontColor) { this.combineUpright = enabled;
setFontColor(style.fontColor); }
}
if (style.bold != UNSPECIFIED) { public boolean getCombineUpright() {
bold = style.bold; return combineUpright;
}
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);
}
} }
private static int updateScoreForMatch( private static int updateScoreForMatch(

View file

@ -15,11 +15,11 @@
*/ */
package com.google.android.exoplayer2.text.webvtt; package com.google.android.exoplayer2.text.webvtt;
import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan;
import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.text.Layout; import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.SpannedString; import android.text.SpannedString;
@ -37,6 +37,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue; 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.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; 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 ENTITY_NON_BREAK_SPACE = "nbsp";
private static final String TAG_BOLD = "b"; 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_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_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_BOLD = Typeface.BOLD;
private static final int STYLE_ITALIC = Typeface.ITALIC; private static final int STYLE_ITALIC = Typeface.ITALIC;
@ -197,6 +201,7 @@ public final class WebvttCueParser {
ArrayDeque<StartTag> startTagStack = new ArrayDeque<>(); ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
List<StyleMatch> scratchStyleMatches = new ArrayList<>(); List<StyleMatch> scratchStyleMatches = new ArrayList<>();
int pos = 0; int pos = 0;
List<Element> nestedElements = new ArrayList<>();
while (pos < markup.length()) { while (pos < markup.length()) {
char curr = markup.charAt(pos); char curr = markup.charAt(pos);
switch (curr) { switch (curr) {
@ -225,8 +230,14 @@ public final class WebvttCueParser {
break; break;
} }
startTag = startTagStack.pop(); startTag = startTagStack.pop();
applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches); applySpansForTag(
} while(!startTag.name.equals(tagName)); 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) { } else if (!isVoidTag) {
startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length())); startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
} }
@ -256,9 +267,15 @@ public final class WebvttCueParser {
} }
// apply unclosed tags // apply unclosed tags
while (!startTagStack.isEmpty()) { 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); scratchStyleMatches);
return SpannedString.valueOf(spannedText); return SpannedString.valueOf(spannedText);
} }
@ -442,6 +459,8 @@ public final class WebvttCueParser {
case TAG_CLASS: case TAG_CLASS:
case TAG_ITALIC: case TAG_ITALIC:
case TAG_LANG: case TAG_LANG:
case TAG_RUBY:
case TAG_RUBY_TEXT:
case TAG_UNDERLINE: case TAG_UNDERLINE:
case TAG_VOICE: case TAG_VOICE:
return true; return true;
@ -453,6 +472,7 @@ public final class WebvttCueParser {
private static void applySpansForTag( private static void applySpansForTag(
@Nullable String cueId, @Nullable String cueId,
StartTag startTag, StartTag startTag,
List<Element> nestedElements,
SpannableStringBuilder text, SpannableStringBuilder text,
List<WebvttCssStyle> styles, List<WebvttCssStyle> styles,
List<StyleMatch> scratchStyleMatches) { List<StyleMatch> scratchStyleMatches) {
@ -467,6 +487,29 @@ public final class WebvttCueParser {
text.setSpan(new StyleSpan(STYLE_ITALIC), start, end, text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; 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: case TAG_UNDERLINE:
text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
@ -492,7 +535,11 @@ public final class WebvttCueParser {
return; return;
} }
if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) { 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.isLinethrough()) { if (style.isLinethrough()) {
@ -502,39 +549,71 @@ public final class WebvttCueParser {
spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasFontColor()) { if (style.hasFontColor()) {
spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, addOrReplaceSpan(
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); spannedText,
new ForegroundColorSpan(style.getFontColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.hasBackgroundColor()) { if (style.hasBackgroundColor()) {
spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, addOrReplaceSpan(
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); spannedText,
new BackgroundColorSpan(style.getBackgroundColor()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getFontFamily() != null) { if (style.getFontFamily() != null) {
spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, addOrReplaceSpan(
spannedText,
new TypefaceSpan(style.getFontFamily()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
Layout.Alignment textAlign = style.getTextAlign(); Layout.Alignment textAlign = style.getTextAlign();
if (textAlign != null) { if (textAlign != null) {
spannedText.setSpan( addOrReplaceSpan(
new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spannedText,
new AlignmentSpan.Standard(textAlign),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
switch (style.getFontSizeUnit()) { switch (style.getFontSizeUnit()) {
case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case WebvttCssStyle.FONT_SIZE_UNIT_EM: 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT: 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); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
case WebvttCssStyle.UNSPECIFIED: case WebvttCssStyle.UNSPECIFIED:
// Do nothing. // Do nothing.
break; 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_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_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]);
result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); 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( result.append(
C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); 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; return result;
} }

View file

@ -1355,6 +1355,7 @@ public final class Util {
public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { public static boolean isEncodingLinearPcm(@C.Encoding int encoding) {
return encoding == C.ENCODING_PCM_8BIT return encoding == C.ENCODING_PCM_8BIT
|| encoding == C.ENCODING_PCM_16BIT || encoding == C.ENCODING_PCM_16BIT
|| encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN
|| encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_24BIT
|| encoding == C.ENCODING_PCM_32BIT || encoding == C.ENCODING_PCM_32BIT
|| encoding == C.ENCODING_PCM_FLOAT; || encoding == C.ENCODING_PCM_FLOAT;
@ -1423,14 +1424,13 @@ public final class Util {
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
return channelCount; return channelCount;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
return channelCount * 2; return channelCount * 2;
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:
return channelCount * 3; return channelCount * 3;
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
return channelCount * 4; return channelCount * 4;
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
@ -2126,6 +2126,8 @@ public final class Util {
return C.NETWORK_TYPE_3G; return C.NETWORK_TYPE_3G;
case TelephonyManager.NETWORK_TYPE_LTE: case TelephonyManager.NETWORK_TYPE_LTE:
return C.NETWORK_TYPE_4G; return C.NETWORK_TYPE_4G;
case TelephonyManager.NETWORK_TYPE_NR:
return C.NETWORK_TYPE_5G;
case TelephonyManager.NETWORK_TYPE_IWLAN: case TelephonyManager.NETWORK_TYPE_IWLAN:
return C.NETWORK_TYPE_WIFI; return C.NETWORK_TYPE_WIFI;
case TelephonyManager.NETWORK_TYPE_GSM: case TelephonyManager.NETWORK_TYPE_GSM:

View file

@ -31,13 +31,13 @@ track 1:
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1
data = length 59, hash A0217393 data = length 59, hash 1AD38625
sample 1: sample 1:
time = 2345000 time = 2345000
flags = 1 flags = 1
data = length 95, hash 4904F2 data = length 95, hash F331C282
sample 2: sample 2:
time = 4567000 time = 4567000
flags = 1 flags = 1
data = length 59, hash EFAB6D8A data = length 59, hash F8CD7C60
tracksEnded = true tracksEnded = true

View file

@ -31,13 +31,13 @@ track 1:
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1
data = length 59, hash A0217393 data = length 59, hash 1AD38625
sample 1: sample 1:
time = 2345000 time = 2345000
flags = 1 flags = 1
data = length 95, hash 4904F2 data = length 95, hash F331C282
sample 2: sample 2:
time = 4567000 time = 4567000
flags = 1 flags = 1
data = length 59, hash EFAB6D8A data = length 59, hash F8CD7C60
tracksEnded = true tracksEnded = true

View file

@ -31,13 +31,13 @@ track 1:
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1
data = length 59, hash A0217393 data = length 59, hash 1AD38625
sample 1: sample 1:
time = 2345000 time = 2345000
flags = 1 flags = 1
data = length 95, hash 4904F2 data = length 95, hash F331C282
sample 2: sample 2:
time = 4567000 time = 4567000
flags = 1 flags = 1
data = length 59, hash EFAB6D8A data = length 59, hash F8CD7C60
tracksEnded = true tracksEnded = true

View file

@ -31,13 +31,13 @@ track 1:
sample 0: sample 0:
time = 0 time = 0
flags = 1 flags = 1
data = length 59, hash A0217393 data = length 59, hash 1AD38625
sample 1: sample 1:
time = 2345000 time = 2345000
flags = 1 flags = 1
data = length 95, hash 4904F2 data = length 95, hash F331C282
sample 2: sample 2:
time = 4567000 time = 4567000
flags = 1 flags = 1
data = length 59, hash EFAB6D8A data = length 59, hash F8CD7C60
tracksEnded = true 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 WEBVTT
STYLE STYLE
::cue(\n#id ){text-decoration:underline;} ::cue(#id ){text-decoration:underline;}
STYLE STYLE
::cue(#id.class1.class2 ){ color: violet;} ::cue(#id.class1.class2 ){ color: violet;}
@ -20,7 +20,7 @@ STYLE
id id
00:00.000 --> 00:01.001 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 íd
00:02.000 --> 00:02.001 00:02.000 --> 00:02.001
@ -31,10 +31,10 @@ _id
This <lang.class.another>should be courier and bold. This <lang.class.another>should be courier and bold.
00:04.000 --> 00:04.001 00:04.000 --> 00:04.001
This <v Strider Trancos> shouldn't be bold.</v> This <v Strider Trancos>shouldn't be bold.</v>
This <v.class.clazz Strider Trancos> should be bold. This <v.class.clazz Strider Trancos>should be bold.
anId anId
00:05.000 --> 00:05.001 00:05.000 --> 00:05.001
This is <v.class1.class3.class2 Pipo> specific </v> This is <v.class1.class3.class2 Pipo>specific</v>
<v.class1.class3.class2 Robert> But this is more italic</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 NOTE Wrong position provided. It should be provided as
a percentage value 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. This is the second subtitle.
NOTE Line as percentage and line alignment 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. This is the third subtitle.
NOTE Line as absolute negative number and without line alignment. 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. 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. This is the fifth subtitle.
NOTE In newer drafts, align:middle has been replaced by align:center 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. 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; 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 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.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -66,9 +61,7 @@ public class FlacExtractorTest {
@Test @Test
public void testOneMetadataBlock() throws Exception { public void testOneMetadataBlock() throws Exception {
// Don't simulate IO errors as it is too slow when using the binary search seek map (see ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac");
// [Internal: b/145994869]).
assertBehaviorWithoutSimulatingIOErrors("flac/bear_one_metadata_block.flac");
} }
@Test @Test
@ -85,61 +78,4 @@ public class FlacExtractorTest {
public void testUncommonSampleRate() throws Exception { public void testUncommonSampleRate() throws Exception {
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac"); 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"); 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) { private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats);
} }

View file

@ -42,4 +42,9 @@ public final class Mp4ExtractorTest {
public void testMp4SampleWithMdatTooLong() throws Exception { public void testMp4SampleWithMdatTooLong() throws Exception {
ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4"); 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 static com.google.common.truth.Truth.assertThat;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
@ -172,6 +173,7 @@ public final class TsExtractorTest {
} }
} }
@Nullable
@Override @Override
public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) { public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
if (provideCustomEsReader && streamType == 3) { if (provideCustomEsReader && streamType == 3) {

View file

@ -28,4 +28,9 @@ public final class WavExtractorTest {
public void testSample() throws Exception { public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav"); 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.areEqual;
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
import static com.google.common.truth.Truth.assertThat; 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.MediaCodec;
import android.media.MediaFormat; import android.media.MediaFormat;
@ -29,7 +29,7 @@ import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -45,27 +45,32 @@ public class AsynchronousMediaCodecAdapterTest {
private MediaCodec.BufferInfo bufferInfo; private MediaCodec.BufferInfo bufferInfo;
@Before @Before
public void setup() throws IOException { public void setUp() throws IOException {
handlerThread = new HandlerThread("TestHandlerThread"); handlerThread = new HandlerThread("TestHandlerThread");
handlerThread.start(); handlerThread.start();
looper = handlerThread.getLooper(); looper = handlerThread.getLooper();
codec = MediaCodec.createByCodecName("h264"); codec = MediaCodec.createByCodecName("h264");
adapter = new AsynchronousMediaCodecAdapter(codec, looper); adapter = new AsynchronousMediaCodecAdapter(codec, looper);
adapter.setCodecStartRunnable(() -> {});
bufferInfo = new MediaCodec.BufferInfo(); bufferInfo = new MediaCodec.BufferInfo();
} }
@After @After
public void tearDown() { public void tearDown() {
adapter.shutdown();
handlerThread.quit(); handlerThread.quit();
} }
@Test @Test
public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() { public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
adapter.start();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
} }
@Test @Test
public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() { public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
adapter.start();
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
@ -73,6 +78,7 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() { public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() {
adapter.start();
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
adapter.flush(); adapter.flush();
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1); adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1);
@ -83,9 +89,7 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer() public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer()
throws InterruptedException { throws InterruptedException {
// Disable calling codec.start() after flush() completes to avoid receiving buffers from the adapter.start();
// shadow codec impl
adapter.setOnCodecStart(() -> {});
Handler handler = new Handler(looper); Handler handler = new Handler(looper);
handler.post( handler.post(
() -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0)); () -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0));
@ -100,28 +104,35 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException() public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException()
throws InterruptedException { 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(); adapter.flush();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
try { assertThrows(
adapter.dequeueInputBufferIndex(); IllegalStateException.class,
fail(); () -> {
} catch (IllegalStateException expected) { adapter.dequeueInputBufferIndex();
} });
} }
@Test @Test
public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() { public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() {
adapter.start();
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo)) assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); .isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
} }
@Test @Test
public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() { public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
adapter.start();
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
outBufferInfo.presentationTimeUs = 10; outBufferInfo.presentationTimeUs = 10;
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo);
@ -132,6 +143,7 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() { public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() {
adapter.start();
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo);
adapter.flush(); adapter.flush();
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo); adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo);
@ -143,9 +155,7 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer() public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer()
throws InterruptedException { throws InterruptedException {
// Disable calling codec.start() after flush() completes to avoid receiving buffers from the adapter.start();
// shadow codec impl
adapter.setOnCodecStart(() -> {});
Handler handler = new Handler(looper); Handler handler = new Handler(looper);
MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo();
handler.post( handler.post(
@ -164,31 +174,23 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException() public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException()
throws InterruptedException { 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(); adapter.flush();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
}
@Test
public void getOutputFormat_withoutFormat_throwsException() {
try {
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() { public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() {
adapter.start();
MediaFormat[] formats = new MediaFormat[10]; MediaFormat[] formats = new MediaFormat[10];
MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback(); MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback();
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
@ -212,6 +214,7 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException { public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException {
adapter.start();
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format); adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format);
adapter.dequeueOutputBufferIndex(bufferInfo); adapter.dequeueOutputBufferIndex(bufferInfo);
@ -223,13 +226,13 @@ public class AsynchronousMediaCodecAdapterTest {
@Test @Test
public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException { public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException {
AtomicBoolean onCodecStartCalled = new AtomicBoolean(false); AtomicInteger onCodecStartCalled = new AtomicInteger(0);
Runnable onCodecStart = () -> onCodecStartCalled.set(true); adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet());
adapter.setOnCodecStart(onCodecStart); adapter.start();
adapter.flush(); adapter.flush();
adapter.shutdown(); adapter.shutdown();
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); 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.areEqual;
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
import static com.google.common.truth.Truth.assertThat; 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 static org.robolectric.Shadows.shadowOf;
import android.media.MediaCodec; import android.media.MediaCodec;
@ -47,16 +47,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
private MediaCodec.BufferInfo bufferInfo = null; private MediaCodec.BufferInfo bufferInfo = null;
@Before @Before
public void setup() throws IOException { public void setUp() throws IOException {
codec = MediaCodec.createByCodecName("h264"); codec = MediaCodec.createByCodecName("h264");
handlerThread = new TestHandlerThread("TestHandlerThread"); handlerThread = new TestHandlerThread("TestHandlerThread");
adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread); adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread);
adapter.setCodecStartRunnable(() -> {});
bufferInfo = new MediaCodec.BufferInfo(); bufferInfo = new MediaCodec.BufferInfo();
} }
@After @After
public void tearDown() { public void tearDown() {
adapter.shutdown(); adapter.shutdown();
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
} }
@ -66,42 +68,15 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
adapter.shutdown(); 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 @Test
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
throws InterruptedException { 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.start();
adapter.flush(); adapter.flush();
@ -110,11 +85,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
waitUntilAllEventsAreExecuted( waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue(); .isTrue();
try {
adapter.dequeueInputBufferIndex(); assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
@ -144,9 +116,6 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
@Test @Test
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer()
throws InterruptedException { throws InterruptedException {
// Disable calling codec.start() after flush to avoid receiving buffers from the
// shadow codec impl
adapter.setOnCodecStart(() -> {});
adapter.start(); adapter.start();
Looper looper = handlerThread.getLooper(); Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper); Handler handler = new Handler(looper);
@ -169,39 +138,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
adapter.start(); adapter.start();
adapter.onMediaCodecError(new IllegalStateException("error from codec")); adapter.onMediaCodecError(new IllegalStateException("error from codec"));
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
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) {
}
} }
@Test @Test
public void dequeueOutputBufferIndex_withInternalException_throwsException() public void dequeueOutputBufferIndex_withInternalException_throwsException()
throws InterruptedException { 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.start();
adapter.flush(); adapter.flush();
@ -210,11 +158,7 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
waitUntilAllEventsAreExecuted( waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue(); .isTrue();
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
@ -275,42 +219,14 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
adapter.start(); adapter.start();
adapter.onMediaCodecError(new IllegalStateException("error from codec")); adapter.onMediaCodecError(new IllegalStateException("error from codec"));
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
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) {
}
} }
@Test @Test
public void getOutputFormat_withoutFormatReceived_throwsException() { public void getOutputFormat_withoutFormatReceived_throwsException() {
adapter.start(); adapter.start();
try { assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat());
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
@ -351,28 +267,10 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
assertThat(adapter.getOutputFormat()).isEqualTo(format); 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 @Test
public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException {
AtomicInteger onCodecStartCount = new AtomicInteger(0); AtomicInteger codecStartCalls = new AtomicInteger(0);
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
adapter.start(); adapter.start();
Looper looper = handlerThread.getLooper(); Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper); Handler handler = new Handler(looper);
@ -384,23 +282,23 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
adapter.flush(); // Enqueues a second flush event adapter.flush(); // Enqueues a second flush event
handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
// Progress the looper until the milestoneCount is increased - first flush event // Progress the looper until the milestoneCount is increased.
// should have been a no-op // adapter.start() will call codec.start(). First flush event should not call codec.start().
ShadowLooper shadowLooper = shadowOf(looper); ShadowLooper shadowLooper = shadowOf(looper);
while (milestoneCount.get() < 1) { while (milestoneCount.get() < 1) {
shadowLooper.runOneTask(); shadowLooper.runOneTask();
} }
assertThat(onCodecStartCount.get()).isEqualTo(0); assertThat(codecStartCalls.get()).isEqualTo(1);
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
assertThat(onCodecStartCount.get()).isEqualTo(1); assertThat(codecStartCalls.get()).isEqualTo(2);
} }
@Test @Test
public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException {
AtomicInteger onCodecStartCount = new AtomicInteger(0); AtomicInteger onCodecStartCount = new AtomicInteger(0);
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet());
adapter.start(); adapter.start();
// Obtain looper when adapter is started // Obtain looper when adapter is started
Looper looper = handlerThread.getLooper(); Looper looper = handlerThread.getLooper();
@ -408,8 +306,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
adapter.shutdown(); adapter.shutdown();
assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue();
// only shutdown flushes the MediaCodecAsync handler // Only adapter.start() calls onCodecStart.
assertThat(onCodecStartCount.get()).isEqualTo(0); assertThat(onCodecStartCount.get()).isEqualTo(1);
} }
private static class TestHandlerThread extends HandlerThread { 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.areEqual;
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted; import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
import static com.google.common.truth.Truth.assertThat; 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 static org.robolectric.Shadows.shadowOf;
import android.media.MediaCodec; import android.media.MediaCodec;
@ -44,20 +44,21 @@ public class MultiLockAsyncMediaCodecAdapterTest {
private MultiLockAsyncMediaCodecAdapter adapter; private MultiLockAsyncMediaCodecAdapter adapter;
private MediaCodec codec; private MediaCodec codec;
private MediaCodec.BufferInfo bufferInfo = null; private MediaCodec.BufferInfo bufferInfo = null;
private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy;
private TestHandlerThread handlerThread; private TestHandlerThread handlerThread;
@Before @Before
public void setup() throws IOException { public void setUp() throws IOException {
codec = MediaCodec.createByCodecName("h264"); codec = MediaCodec.createByCodecName("h264");
handlerThread = new TestHandlerThread("TestHandlerThread"); handlerThread = new TestHandlerThread("TestHandlerThread");
adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread); adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread);
adapter.setCodecStartRunnable(() -> {});
bufferInfo = new MediaCodec.BufferInfo(); bufferInfo = new MediaCodec.BufferInfo();
} }
@After @After
public void tearDown() { public void tearDown() {
adapter.shutdown(); adapter.shutdown();
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0); assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
} }
@ -67,42 +68,15 @@ public class MultiLockAsyncMediaCodecAdapterTest {
adapter.shutdown(); 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 @Test
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException() public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
throws InterruptedException { 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.start();
adapter.flush(); adapter.flush();
@ -111,11 +85,7 @@ public class MultiLockAsyncMediaCodecAdapterTest {
waitUntilAllEventsAreExecuted( waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue(); .isTrue();
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
adapter.dequeueInputBufferIndex();
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
@ -145,9 +115,6 @@ public class MultiLockAsyncMediaCodecAdapterTest {
@Test @Test
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer() public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer()
throws InterruptedException { throws InterruptedException {
// Disable calling codec.start() after flush to avoid receiving buffers from the
// shadow codec impl
adapter.setOnCodecStart(() -> {});
adapter.start(); adapter.start();
Looper looper = handlerThread.getLooper(); Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper); Handler handler = new Handler(looper);
@ -170,39 +137,19 @@ public class MultiLockAsyncMediaCodecAdapterTest {
adapter.start(); adapter.start();
adapter.onMediaCodecError(new IllegalStateException("error from codec")); adapter.onMediaCodecError(new IllegalStateException("error from codec"));
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
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) {
}
}
@Test @Test
public void dequeueOutputBufferIndex_withInternalException_throwsException() public void dequeueOutputBufferIndex_withInternalException_throwsException()
throws InterruptedException { 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.start();
adapter.flush(); adapter.flush();
@ -211,11 +158,7 @@ public class MultiLockAsyncMediaCodecAdapterTest {
waitUntilAllEventsAreExecuted( waitUntilAllEventsAreExecuted(
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS)) handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
.isTrue(); .isTrue();
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
adapter.dequeueOutputBufferIndex(bufferInfo);
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
@ -276,42 +219,14 @@ public class MultiLockAsyncMediaCodecAdapterTest {
adapter.start(); adapter.start();
adapter.onMediaCodecError(new IllegalStateException("error from codec")); adapter.onMediaCodecError(new IllegalStateException("error from codec"));
try { assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
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) {
}
} }
@Test @Test
public void getOutputFormat_withoutFormatReceived_throwsException() { public void getOutputFormat_withoutFormatReceived_throwsException() {
adapter.start(); adapter.start();
try { assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat());
adapter.getOutputFormat();
fail();
} catch (IllegalStateException expected) {
}
} }
@Test @Test
@ -352,28 +267,10 @@ public class MultiLockAsyncMediaCodecAdapterTest {
assertThat(adapter.getOutputFormat()).isEqualTo(format); 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 @Test
public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException { public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException {
AtomicInteger onCodecStartCount = new AtomicInteger(0); AtomicInteger codecStartCalls = new AtomicInteger(0);
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
adapter.start(); adapter.start();
Looper looper = handlerThread.getLooper(); Looper looper = handlerThread.getLooper();
Handler handler = new Handler(looper); Handler handler = new Handler(looper);
@ -385,23 +282,23 @@ public class MultiLockAsyncMediaCodecAdapterTest {
adapter.flush(); // Enqueues a second flush event adapter.flush(); // Enqueues a second flush event
handler.post(() -> adapter.onInputBufferAvailable(codec, 3)); handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
// Progress the looper until the milestoneCount is increased - first flush event // Progress the looper until the milestoneCount is increased:
// should have been a no-op // adapter.start() called codec.start() but first flush event should have been a no-op
ShadowLooper shadowLooper = shadowOf(looper); ShadowLooper shadowLooper = shadowOf(looper);
while (milestoneCount.get() < 1) { while (milestoneCount.get() < 1) {
shadowLooper.runOneTask(); shadowLooper.runOneTask();
} }
assertThat(onCodecStartCount.get()).isEqualTo(0); assertThat(codecStartCalls.get()).isEqualTo(1);
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue(); assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3); assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
assertThat(onCodecStartCount.get()).isEqualTo(1); assertThat(codecStartCalls.get()).isEqualTo(2);
} }
@Test @Test
public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException { public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException {
AtomicInteger onCodecStartCount = new AtomicInteger(0); AtomicInteger codecStartCalls = new AtomicInteger(0);
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet()); adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
adapter.start(); adapter.start();
// Obtain looper when adapter is started. // Obtain looper when adapter is started.
Looper looper = handlerThread.getLooper(); Looper looper = handlerThread.getLooper();
@ -409,8 +306,8 @@ public class MultiLockAsyncMediaCodecAdapterTest {
adapter.shutdown(); adapter.shutdown();
assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue(); assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue();
// Only shutdown flushes the MediaCodecAsync handler. // Only adapter.start() called codec#start()
assertThat(onCodecStartCount.get()).isEqualTo(0); assertThat(codecStartCalls.get()).isEqualTo(1);
} }
private static class TestHandlerThread extends HandlerThread { private static class TestHandlerThread extends HandlerThread {

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