mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Merge branch 'google-dev-v2' into dev-v2-ac4-drm
This commit is contained in:
commit
0ea49df901
112 changed files with 4937 additions and 2300 deletions
|
|
@ -32,6 +32,30 @@
|
|||
([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling
|
||||
could previously cause downloads to be paused when they should have been able
|
||||
to proceed.
|
||||
* Fix handling of E-AC-3 streams that contain AC-3 syncframes
|
||||
([#6602](https://github.com/google/ExoPlayer/issues/6602)).
|
||||
* Fix playback of TrueHD streams in Matroska
|
||||
([#6845](https://github.com/google/ExoPlayer/issues/6845)).
|
||||
* Support "twos" codec (big endian PCM) in MP4
|
||||
([#5789](https://github.com/google/ExoPlayer/issues/5789)).
|
||||
* WAV: Support IMA ADPCM encoded data.
|
||||
* Show ad group markers in `DefaultTimeBar` even if they are after the end of
|
||||
the current window
|
||||
([#6552](https://github.com/google/ExoPlayer/issues/6552)).
|
||||
* WAV:
|
||||
* Support IMA ADPCM encoded data.
|
||||
* Improve support for G.711 A-law and mu-law encoded data.
|
||||
* Fix MKV subtitles to disappear when intended instead of lasting until the
|
||||
next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)).
|
||||
* Parse \<ruby\> and \<rt\> tags in WebVTT subtitles (rendering is coming
|
||||
later).
|
||||
* Parse `text-combine-upright` CSS property (i.e. tate-chu-yoko) in WebVTT
|
||||
subtitles (rendering is coming later).
|
||||
* OkHttp extension: Upgrade OkHttp dependency to 3.12.7, which fixes a class of
|
||||
`SocketTimeoutException` issues when using HTTP/2
|
||||
([#4078](https://github.com/google/ExoPlayer/issues/4078)).
|
||||
* Don't use notification chronometer if playback speed is != 1.0
|
||||
([#6816](https://github.com/google/ExoPlayer/issues/6816)).
|
||||
|
||||
### 2.11.1 (2019-12-20) ###
|
||||
|
||||
|
|
|
|||
|
|
@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
|
||||
|| !isOutputSupported(format)) {
|
||||
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@ import java.util.List;
|
|||
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
||||
}
|
||||
Assertions.checkNotNull(format.sampleMimeType);
|
||||
codecName =
|
||||
Assertions.checkNotNull(
|
||||
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
|
||||
codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
|
||||
extraData = getExtraData(format.sampleMimeType, format.initializationData);
|
||||
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
||||
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
||||
|
|
@ -145,16 +143,12 @@ import java.util.List;
|
|||
nativeContext = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel count of output audio. May only be called after {@link #decode}.
|
||||
*/
|
||||
/** Returns the channel count of output audio. */
|
||||
public int getChannelCount() {
|
||||
return channelCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sample rate of output audio. May only be called after {@link #decode}.
|
||||
*/
|
||||
/** Returns the sample rate of output audio. */
|
||||
public int getSampleRate() {
|
||||
return sampleRate;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
|
|
@ -65,13 +64,12 @@ public final class FfmpegLibrary {
|
|||
* Returns whether the underlying library supports the specified MIME type.
|
||||
*
|
||||
* @param mimeType The MIME type to check.
|
||||
* @param encoding The PCM encoding for raw audio.
|
||||
*/
|
||||
public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
|
||||
public static boolean supportsFormat(String mimeType) {
|
||||
if (!isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
String codecName = getCodecName(mimeType, encoding);
|
||||
String codecName = getCodecName(mimeType);
|
||||
if (codecName == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -86,7 +84,7 @@ public final class FfmpegLibrary {
|
|||
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
|
||||
* if it's unsupported.
|
||||
*/
|
||||
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
|
||||
/* package */ static @Nullable String getCodecName(String mimeType) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
return "aac";
|
||||
|
|
@ -116,14 +114,10 @@ public final class FfmpegLibrary {
|
|||
return "flac";
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
return "alac";
|
||||
case MimeTypes.AUDIO_RAW:
|
||||
if (encoding == C.ENCODING_PCM_MU_LAW) {
|
||||
return "pcm_mulaw";
|
||||
} else if (encoding == C.ENCODING_PCM_A_LAW) {
|
||||
return "pcm_alaw";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
case MimeTypes.AUDIO_MLAW:
|
||||
return "pcm_mulaw";
|
||||
case MimeTypes.AUDIO_ALAW:
|
||||
return "pcm_alaw";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,8 @@ import java.nio.ByteBuffer;
|
|||
if (targetSampleInLastFrame) {
|
||||
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
|
||||
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
|
||||
// The input position is passed even though it does not indicate the frame containing the
|
||||
// target sample because the extractor must continue to read from this position.
|
||||
return TimestampSearchResult.targetFoundResult(input.getPosition());
|
||||
} else if (nextFrameSampleIndex <= targetSampleIndex) {
|
||||
return TimestampSearchResult.underestimatedResult(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,9 +39,9 @@ dependencies {
|
|||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
// Do not update to 3.13.X or later until minSdkVersion is increased to 21:
|
||||
// https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5
|
||||
// Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop
|
||||
// us from making this mistake!
|
||||
api 'com.squareup.okhttp3:okhttp:3.12.5'
|
||||
// Since OkHttp is distributed as a jar rather than an aar, Gradle won't
|
||||
// stop us from making this mistake!
|
||||
api 'com.squareup.okhttp3:okhttp:3.12.7'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
public static android.net.Uri buildRawResourceUri(int);
|
||||
}
|
||||
|
||||
# Methods accessed via reflection in DefaultExtractorsFactory
|
||||
-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary {
|
||||
public static boolean isAvailable();
|
||||
}
|
||||
|
||||
# Some members of this class are being accessed from native methods. Keep them unobfuscated.
|
||||
-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer {
|
||||
*;
|
||||
|
|
|
|||
|
|
@ -150,10 +150,10 @@ public final class C {
|
|||
/**
|
||||
* Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},
|
||||
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
|
||||
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
|
||||
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_MP3}, {@link
|
||||
* #ENCODING_AC3}, {@link #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4},
|
||||
* {@link #ENCODING_DTS}, {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
|
||||
* #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
|
||||
* {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link
|
||||
* #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
|
||||
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -162,11 +162,10 @@ public final class C {
|
|||
ENCODING_INVALID,
|
||||
ENCODING_PCM_8BIT,
|
||||
ENCODING_PCM_16BIT,
|
||||
ENCODING_PCM_16BIT_BIG_ENDIAN,
|
||||
ENCODING_PCM_24BIT,
|
||||
ENCODING_PCM_32BIT,
|
||||
ENCODING_PCM_FLOAT,
|
||||
ENCODING_PCM_MU_LAW,
|
||||
ENCODING_PCM_A_LAW,
|
||||
ENCODING_MP3,
|
||||
ENCODING_AC3,
|
||||
ENCODING_E_AC3,
|
||||
|
|
@ -174,15 +173,15 @@ public final class C {
|
|||
ENCODING_AC4,
|
||||
ENCODING_DTS,
|
||||
ENCODING_DTS_HD,
|
||||
ENCODING_DOLBY_TRUEHD,
|
||||
ENCODING_DOLBY_TRUEHD
|
||||
})
|
||||
public @interface Encoding {}
|
||||
|
||||
/**
|
||||
* Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},
|
||||
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
|
||||
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
|
||||
* #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}.
|
||||
* #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
|
||||
* {@link #ENCODING_PCM_FLOAT}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -191,11 +190,10 @@ public final class C {
|
|||
ENCODING_INVALID,
|
||||
ENCODING_PCM_8BIT,
|
||||
ENCODING_PCM_16BIT,
|
||||
ENCODING_PCM_16BIT_BIG_ENDIAN,
|
||||
ENCODING_PCM_24BIT,
|
||||
ENCODING_PCM_32BIT,
|
||||
ENCODING_PCM_FLOAT,
|
||||
ENCODING_PCM_MU_LAW,
|
||||
ENCODING_PCM_A_LAW
|
||||
ENCODING_PCM_FLOAT
|
||||
})
|
||||
public @interface PcmEncoding {}
|
||||
/** @see AudioFormat#ENCODING_INVALID */
|
||||
|
|
@ -204,16 +202,14 @@ public final class C {
|
|||
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
|
||||
/** @see AudioFormat#ENCODING_PCM_16BIT */
|
||||
public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
|
||||
/** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */
|
||||
public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000;
|
||||
/** PCM encoding with 24 bits per sample. */
|
||||
public static final int ENCODING_PCM_24BIT = 0x80000000;
|
||||
public static final int ENCODING_PCM_24BIT = 0x20000000;
|
||||
/** PCM encoding with 32 bits per sample. */
|
||||
public static final int ENCODING_PCM_32BIT = 0x40000000;
|
||||
public static final int ENCODING_PCM_32BIT = 0x30000000;
|
||||
/** @see AudioFormat#ENCODING_PCM_FLOAT */
|
||||
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
|
||||
/** Audio encoding for mu-law. */
|
||||
public static final int ENCODING_PCM_MU_LAW = 0x10000000;
|
||||
/** Audio encoding for A-law. */
|
||||
public static final int ENCODING_PCM_A_LAW = 0x20000000;
|
||||
/** @see AudioFormat#ENCODING_MP3 */
|
||||
public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
|
||||
/** @see AudioFormat#ENCODING_AC3 */
|
||||
|
|
@ -981,8 +977,8 @@ public final class C {
|
|||
/**
|
||||
* Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE},
|
||||
* {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link
|
||||
* #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or
|
||||
* {@link #NETWORK_TYPE_OTHER}.
|
||||
* #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link
|
||||
* #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -993,6 +989,7 @@ public final class C {
|
|||
NETWORK_TYPE_2G,
|
||||
NETWORK_TYPE_3G,
|
||||
NETWORK_TYPE_4G,
|
||||
NETWORK_TYPE_5G,
|
||||
NETWORK_TYPE_CELLULAR_UNKNOWN,
|
||||
NETWORK_TYPE_ETHERNET,
|
||||
NETWORK_TYPE_OTHER
|
||||
|
|
@ -1010,6 +1007,8 @@ public final class C {
|
|||
public static final int NETWORK_TYPE_3G = 4;
|
||||
/** Network type for a 4G cellular connection. */
|
||||
public static final int NETWORK_TYPE_4G = 5;
|
||||
/** Network type for a 5G cellular connection. */
|
||||
public static final int NETWORK_TYPE_5G = 9;
|
||||
/**
|
||||
* Network type for cellular connections which cannot be mapped to one of {@link
|
||||
* #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.
|
||||
|
|
@ -1017,10 +1016,7 @@ public final class C {
|
|||
public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;
|
||||
/** Network type for an Ethernet connection. */
|
||||
public static final int NETWORK_TYPE_ETHERNET = 7;
|
||||
/**
|
||||
* Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,
|
||||
* Bluetooth).
|
||||
*/
|
||||
/** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */
|
||||
public static final int NETWORK_TYPE_OTHER = 8;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -138,13 +138,7 @@ public final class Format implements Parcelable {
|
|||
* The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
|
||||
*/
|
||||
public final int sampleRate;
|
||||
/**
|
||||
* The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}
|
||||
* then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link
|
||||
* C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link
|
||||
* C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other
|
||||
* media types.
|
||||
*/
|
||||
/** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */
|
||||
public final @C.PcmEncoding int pcmEncoding;
|
||||
/**
|
||||
* The number of frames to trim from the start of the decoded audio stream, or 0 if not
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ public final class PlaybackStatsListener
|
|||
@Player.State private int playbackState;
|
||||
private boolean isSuppressed;
|
||||
private float playbackSpeed;
|
||||
private boolean isSeeking;
|
||||
|
||||
/**
|
||||
* Creates listener for playback stats.
|
||||
|
|
@ -169,6 +170,9 @@ public final class PlaybackStatsListener
|
|||
@Override
|
||||
public void onSessionCreated(EventTime eventTime, String session) {
|
||||
PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
|
||||
if (isSeeking) {
|
||||
tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true);
|
||||
}
|
||||
tracker.onPlayerStateChanged(
|
||||
eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true);
|
||||
tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true);
|
||||
|
|
@ -288,20 +292,20 @@ public final class PlaybackStatsListener
|
|||
public void onSeekStarted(EventTime eventTime) {
|
||||
sessionManager.updateSessions(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers.get(session).onSeekStarted(eventTime);
|
||||
}
|
||||
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback);
|
||||
}
|
||||
isSeeking = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekProcessed(EventTime eventTime) {
|
||||
sessionManager.updateSessions(eventTime);
|
||||
for (String session : playbackStatsTrackers.keySet()) {
|
||||
if (sessionManager.belongsToSession(eventTime, session)) {
|
||||
playbackStatsTrackers.get(session).onSeekProcessed(eventTime);
|
||||
}
|
||||
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
|
||||
playbackStatsTrackers.get(session).onSeekProcessed(eventTime, belongsToPlayback);
|
||||
}
|
||||
isSeeking = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -563,23 +567,27 @@ public final class PlaybackStatsListener
|
|||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of the start of a seek in the current playback.
|
||||
* Notifies the tracker of the start of a seek, including all seeks while the playback is not in
|
||||
* the foreground.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
|
||||
*/
|
||||
public void onSeekStarted(EventTime eventTime) {
|
||||
public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) {
|
||||
isSeeking = true;
|
||||
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
|
||||
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the tracker of a seek has been processed in the current playback.
|
||||
* Notifies the tracker that a seek has been processed, including all seeks while the playback
|
||||
* is not in the foreground.
|
||||
*
|
||||
* @param eventTime The {@link EventTime}.
|
||||
* @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
|
||||
*/
|
||||
public void onSeekProcessed(EventTime eventTime) {
|
||||
public void onSeekProcessed(EventTime eventTime, boolean belongsToPlayback) {
|
||||
isSeeking = false;
|
||||
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
|
||||
maybeUpdatePlaybackState(eventTime, belongsToPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -875,7 +883,7 @@ public final class PlaybackStatsListener
|
|||
return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED
|
||||
? PlaybackStats.PLAYBACK_STATE_ENDED
|
||||
: PlaybackStats.PLAYBACK_STATE_ABANDONED;
|
||||
} else if (isSeeking) {
|
||||
} else if (isSeeking && isForeground) {
|
||||
// Seeking takes precedence over errors such that we report a seek while in error state.
|
||||
return PlaybackStats.PLAYBACK_STATE_SEEKING;
|
||||
} else if (hasFatalError) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import java.nio.ByteBuffer;
|
|||
|
||||
/**
|
||||
* Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the
|
||||
* definition in ETSI TS 102 366 V1.2.1.
|
||||
* definition in ETSI TS 102 366 V1.4.1.
|
||||
*/
|
||||
public final class Ac3Util {
|
||||
|
||||
|
|
@ -39,8 +39,8 @@ public final class Ac3Util {
|
|||
public static final class SyncFrameInfo {
|
||||
|
||||
/**
|
||||
* AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED},
|
||||
* {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.
|
||||
* AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link
|
||||
* #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -114,9 +114,7 @@ public final class Ac3Util {
|
|||
* The number of new samples per (E-)AC-3 audio block.
|
||||
*/
|
||||
private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;
|
||||
/**
|
||||
* Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.
|
||||
*/
|
||||
/** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */
|
||||
private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK;
|
||||
/**
|
||||
* Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod.
|
||||
|
|
@ -134,20 +132,21 @@ public final class Ac3Util {
|
|||
* Channel counts, indexed by acmod.
|
||||
*/
|
||||
private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
|
||||
/**
|
||||
* Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)
|
||||
*/
|
||||
private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96,
|
||||
112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640};
|
||||
/**
|
||||
* 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)
|
||||
*/
|
||||
private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104,
|
||||
121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};
|
||||
/** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */
|
||||
private static final int[] BITRATE_BY_HALF_FRMSIZECOD =
|
||||
new int[] {
|
||||
32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640
|
||||
};
|
||||
/** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */
|
||||
private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 =
|
||||
new int[] {
|
||||
69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253,
|
||||
1393
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS
|
||||
* 102 366 Annex F. The reading position of {@code data} will be modified.
|
||||
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F.
|
||||
* The reading position of {@code data} will be modified.
|
||||
*
|
||||
* @param data The AC3SpecificBox to parse.
|
||||
* @param trackId The track identifier to set on the format.
|
||||
|
|
@ -179,8 +178,8 @@ public final class Ac3Util {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS
|
||||
* 102 366 Annex F. The reading position of {@code data} will be modified.
|
||||
* Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex
|
||||
* F. The reading position of {@code data} will be modified.
|
||||
*
|
||||
* @param data The EC3SpecificBox to parse.
|
||||
* @param trackId The track identifier to set on the format.
|
||||
|
|
@ -243,9 +242,10 @@ public final class Ac3Util {
|
|||
public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
|
||||
int initialPosition = data.getPosition();
|
||||
data.skipBits(40);
|
||||
boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6.
|
||||
// Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
|
||||
boolean isEac3 = data.readBits(5) > 10;
|
||||
data.setPosition(initialPosition);
|
||||
String mimeType;
|
||||
@Nullable String mimeType;
|
||||
@StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
|
||||
int sampleRate;
|
||||
int acmod;
|
||||
|
|
@ -254,7 +254,7 @@ public final class Ac3Util {
|
|||
boolean lfeon;
|
||||
int channelCount;
|
||||
if (isEac3) {
|
||||
// Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2.
|
||||
// Subsection E.1.2.
|
||||
data.skipBits(16); // syncword
|
||||
switch (data.readBits(2)) { // strmtyp
|
||||
case 0:
|
||||
|
|
@ -472,7 +472,8 @@ public final class Ac3Util {
|
|||
if (data.length < 6) {
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6.
|
||||
// Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
|
||||
boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10;
|
||||
if (isEac3) {
|
||||
int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits.
|
||||
frmsiz |= data[3] & 0xFF; // Least significant 8 bits.
|
||||
|
|
@ -485,24 +486,22 @@ public final class Ac3Util {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the number of audio samples in an AC-3 syncframe.
|
||||
*/
|
||||
public static int getAc3SyncframeAudioSampleCount() {
|
||||
return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's
|
||||
* Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's
|
||||
* position is not modified.
|
||||
*
|
||||
* @param buffer The {@link ByteBuffer} from which to read the syncframe.
|
||||
* @return The number of audio samples represented by the syncframe.
|
||||
*/
|
||||
public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
|
||||
// See ETSI TS 102 366 subsection E.1.2.2.
|
||||
int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;
|
||||
return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6
|
||||
: BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);
|
||||
public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
|
||||
// Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
|
||||
boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10;
|
||||
if (isEac3) {
|
||||
int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;
|
||||
int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4;
|
||||
return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK;
|
||||
} else {
|
||||
return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ public final class Ac4Util {
|
|||
/** The channel count of AC-4 stream. */
|
||||
// TODO: Parse AC-4 stream channel count.
|
||||
private static final int CHANNEL_COUNT_2 = 2;
|
||||
/**
|
||||
* The AC-4 sync frame header size for extractor. The seven bytes are 0xAC, 0x40, 0xFF, 0xFF,
|
||||
* sizeByte1, sizeByte2, sizeByte3. See ETSI TS 103 190-1 V1.3.1, Annex G
|
||||
*/
|
||||
public static final int SAMPLE_HEADER_SIZE = 7;
|
||||
/**
|
||||
* The header size for AC-4 parser. Only needs to be as big as we need to read, not the full
|
||||
* header size.
|
||||
|
|
@ -218,7 +223,7 @@ public final class Ac4Util {
|
|||
/** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */
|
||||
public static void getAc4SampleHeader(int size, ParsableByteArray buffer) {
|
||||
// See ETSI TS 103 190-1 V1.3.1, Annex G.
|
||||
buffer.reset(/* limit= */ 7);
|
||||
buffer.reset(SAMPLE_HEADER_SIZE);
|
||||
buffer.data[0] = (byte) 0xAC;
|
||||
buffer.data[1] = 0x40;
|
||||
buffer.data[2] = (byte) 0xFF;
|
||||
|
|
|
|||
|
|
@ -1149,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
case C.ENCODING_PCM_24BIT:
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
case C.ENCODING_PCM_8BIT:
|
||||
case C.ENCODING_PCM_A_LAW:
|
||||
case C.ENCODING_PCM_FLOAT:
|
||||
case C.ENCODING_PCM_MU_LAW:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
|
|
@ -1166,10 +1164,9 @@ public final class DefaultAudioSink implements AudioSink {
|
|||
case C.ENCODING_DTS_HD:
|
||||
return DtsUtil.parseDtsAudioSampleCount(buffer);
|
||||
case C.ENCODING_AC3:
|
||||
return Ac3Util.getAc3SyncframeAudioSampleCount();
|
||||
case C.ENCODING_E_AC3:
|
||||
case C.ENCODING_E_AC3_JOC:
|
||||
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
|
||||
return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer);
|
||||
case C.ENCODING_AC4:
|
||||
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
|
||||
case C.ENCODING_DOLBY_TRUEHD:
|
||||
|
|
|
|||
|
|
@ -81,7 +81,10 @@ public final class DtsUtil {
|
|||
* @return The DTS format parsed from data in the header.
|
||||
*/
|
||||
public static Format parseDtsFormat(
|
||||
byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) {
|
||||
byte[] frame,
|
||||
@Nullable String trackId,
|
||||
@Nullable String language,
|
||||
@Nullable DrmInitData drmInitData) {
|
||||
ParsableBitArray frameBits = getNormalizedFrameHeader(frame);
|
||||
frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
|
||||
int amode = frameBits.readBits(6);
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10;
|
||||
|
||||
private static final String TAG = "MediaCodecAudioRenderer";
|
||||
/**
|
||||
* Custom key used to indicate bits per sample by some decoders on Vivo devices. For example
|
||||
* OMX.vivo.alac.decoder on the Vivo Z1 Pro.
|
||||
*/
|
||||
private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample";
|
||||
|
||||
private final Context context;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
|
|
@ -566,7 +571,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
mediaFormat.getString(MediaFormat.KEY_MIME));
|
||||
} else {
|
||||
mediaFormat = outputMediaFormat;
|
||||
encoding = getPcmEncoding(inputFormat);
|
||||
if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) {
|
||||
encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY));
|
||||
} else {
|
||||
encoding = getPcmEncoding(inputFormat);
|
||||
}
|
||||
}
|
||||
int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ import java.nio.ByteBuffer;
|
|||
public AudioFormat onConfigure(AudioFormat inputAudioFormat)
|
||||
throws UnhandledAudioFormatException {
|
||||
@C.PcmEncoding int encoding = inputAudioFormat.encoding;
|
||||
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
|
||||
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
|
||||
if (encoding != C.ENCODING_PCM_8BIT
|
||||
&& encoding != C.ENCODING_PCM_16BIT
|
||||
&& encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN
|
||||
&& encoding != C.ENCODING_PCM_24BIT
|
||||
&& encoding != C.ENCODING_PCM_32BIT) {
|
||||
throw new UnhandledAudioFormatException(inputAudioFormat);
|
||||
}
|
||||
return encoding != C.ENCODING_PCM_16BIT
|
||||
|
|
@ -50,6 +53,9 @@ import java.nio.ByteBuffer;
|
|||
case C.ENCODING_PCM_8BIT:
|
||||
resampledSize = size * 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
|
||||
resampledSize = size;
|
||||
break;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
resampledSize = (size / 3) * 2;
|
||||
break;
|
||||
|
|
@ -58,8 +64,6 @@ import java.nio.ByteBuffer;
|
|||
break;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_PCM_FLOAT:
|
||||
case C.ENCODING_PCM_A_LAW:
|
||||
case C.ENCODING_PCM_MU_LAW:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
|
|
@ -70,21 +74,28 @@ import java.nio.ByteBuffer;
|
|||
ByteBuffer buffer = replaceOutputBuffer(resampledSize);
|
||||
switch (inputAudioFormat.encoding) {
|
||||
case C.ENCODING_PCM_8BIT:
|
||||
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
|
||||
// 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
|
||||
for (int i = position; i < limit; i++) {
|
||||
buffer.put((byte) 0);
|
||||
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
|
||||
// Big endian to little endian resampling. Swap the byte order.
|
||||
for (int i = position; i < limit; i += 2) {
|
||||
buffer.put(inputBuffer.get(i + 1));
|
||||
buffer.put(inputBuffer.get(i));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
// 24->16 bit resampling. Drop the least significant byte.
|
||||
// 24 -> 16 bit resampling. Drop the least significant byte.
|
||||
for (int i = position; i < limit; i += 3) {
|
||||
buffer.put(inputBuffer.get(i + 1));
|
||||
buffer.put(inputBuffer.get(i + 2));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
// 32->16 bit resampling. Drop the two least significant bytes.
|
||||
// 32 -> 16 bit resampling. Drop the two least significant bytes.
|
||||
for (int i = position; i < limit; i += 4) {
|
||||
buffer.put(inputBuffer.get(i + 2));
|
||||
buffer.put(inputBuffer.get(i + 3));
|
||||
|
|
@ -92,8 +103,6 @@ import java.nio.ByteBuffer;
|
|||
break;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_PCM_FLOAT:
|
||||
case C.ENCODING_PCM_A_LAW:
|
||||
case C.ENCODING_PCM_MU_LAW:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -32,15 +32,17 @@ public final class WavUtil {
|
|||
public static final int DATA_FOURCC = 0x64617461;
|
||||
|
||||
/** WAVE type value for integer PCM audio data. */
|
||||
private static final int TYPE_PCM = 0x0001;
|
||||
public static final int TYPE_PCM = 0x0001;
|
||||
/** WAVE type value for float PCM audio data. */
|
||||
private static final int TYPE_FLOAT = 0x0003;
|
||||
public static final int TYPE_FLOAT = 0x0003;
|
||||
/** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */
|
||||
private static final int TYPE_A_LAW = 0x0006;
|
||||
public static final int TYPE_ALAW = 0x0006;
|
||||
/** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */
|
||||
private static final int TYPE_MU_LAW = 0x0007;
|
||||
public static final int TYPE_MLAW = 0x0007;
|
||||
/** WAVE type value for IMA ADPCM audio data. */
|
||||
public static final int TYPE_IMA_ADPCM = 0x0011;
|
||||
/** WAVE type value for extended WAVE format. */
|
||||
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
|
||||
public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
|
||||
|
||||
/**
|
||||
* Returns the WAVE format type value for the given {@link C.PcmEncoding}.
|
||||
|
|
@ -57,10 +59,6 @@ public final class WavUtil {
|
|||
case C.ENCODING_PCM_24BIT:
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
return TYPE_PCM;
|
||||
case C.ENCODING_PCM_A_LAW:
|
||||
return TYPE_A_LAW;
|
||||
case C.ENCODING_PCM_MU_LAW:
|
||||
return TYPE_MU_LAW;
|
||||
case C.ENCODING_PCM_FLOAT:
|
||||
return TYPE_FLOAT;
|
||||
case C.ENCODING_INVALID:
|
||||
|
|
@ -81,10 +79,6 @@ public final class WavUtil {
|
|||
return Util.getPcmEncoding(bitsPerSample);
|
||||
case TYPE_FLOAT:
|
||||
return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
|
||||
case TYPE_A_LAW:
|
||||
return C.ENCODING_PCM_A_LAW;
|
||||
case TYPE_MU_LAW:
|
||||
return C.ENCODING_PCM_MU_LAW;
|
||||
default:
|
||||
return C.ENCODING_INVALID;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,10 +64,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||
@Nullable Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
|
||||
try {
|
||||
// LINT.IfChange
|
||||
flacExtensionExtractorConstructor =
|
||||
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
|
||||
.asSubclass(Extractor.class)
|
||||
.getConstructor();
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
boolean isFlacNativeLibraryAvailable =
|
||||
Boolean.TRUE.equals(
|
||||
Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary")
|
||||
.getMethod("isAvailable")
|
||||
.invoke(/* obj= */ null));
|
||||
if (isFlacNativeLibraryAvailable) {
|
||||
flacExtensionExtractorConstructor =
|
||||
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
|
||||
.asSubclass(Extractor.class)
|
||||
.getConstructor();
|
||||
}
|
||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Expected if the app was built without the FLAC extension.
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ public final class FlacFrameReader {
|
|||
* @param data The array to read the data from, whose position must correspond to the block size
|
||||
* bits.
|
||||
* @param blockSizeKey The key in the block size lookup table.
|
||||
* @return The block size in samples.
|
||||
* @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid.
|
||||
*/
|
||||
public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) {
|
||||
switch (blockSizeKey) {
|
||||
|
|
|
|||
|
|
@ -256,15 +256,18 @@ public final class FlacExtractor implements Extractor {
|
|||
|
||||
// Copy more bytes into the buffer.
|
||||
int currentLimit = buffer.limit();
|
||||
int bytesRead =
|
||||
input.read(
|
||||
buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit);
|
||||
boolean foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT;
|
||||
if (!foundEndOfInput) {
|
||||
buffer.setLimit(currentLimit + bytesRead);
|
||||
} else if (buffer.bytesLeft() == 0) {
|
||||
outputSampleMetadata();
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
boolean foundEndOfInput = false;
|
||||
if (currentLimit < BUFFER_LENGTH) {
|
||||
int bytesRead =
|
||||
input.read(
|
||||
buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit);
|
||||
foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT;
|
||||
if (!foundEndOfInput) {
|
||||
buffer.setLimit(currentLimit + bytesRead);
|
||||
} else if (buffer.bytesLeft() == 0) {
|
||||
outputSampleMetadata();
|
||||
return Extractor.RESULT_END_OF_INPUT;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a frame.
|
||||
|
|
@ -272,7 +275,7 @@ public final class FlacExtractor implements Extractor {
|
|||
|
||||
// Skip frame search on the bytes within the minimum frame size.
|
||||
if (currentFrameBytesWritten < minFrameSize) {
|
||||
buffer.skipBytes(Math.min(minFrameSize, buffer.bytesLeft()));
|
||||
buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft()));
|
||||
}
|
||||
|
||||
long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput);
|
||||
|
|
|
|||
|
|
@ -69,9 +69,20 @@ import java.util.Collections;
|
|||
} else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
|
||||
String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
|
||||
: MimeTypes.AUDIO_MLAW;
|
||||
int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;
|
||||
Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,
|
||||
Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);
|
||||
Format format =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
/* sampleMimeType= */ type,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* maxInputSize= */ Format.NO_VALUE,
|
||||
/* channelCount= */ 1,
|
||||
/* sampleRate= */ 8000,
|
||||
/* pcmEncoding= */ Format.NO_VALUE,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
output.format(format);
|
||||
hasOutputFormat = true;
|
||||
} else if (audioFormat != AUDIO_FORMAT_AAC) {
|
||||
|
|
|
|||
|
|
@ -1250,10 +1250,10 @@ public class MatroskaExtractor implements Extractor {
|
|||
if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {
|
||||
if (blockSampleCount > 1) {
|
||||
Log.w(TAG, "Skipping subtitle sample in laced block.");
|
||||
} else if (durationUs == C.TIME_UNSET) {
|
||||
} else if (blockDurationUs == C.TIME_UNSET) {
|
||||
Log.w(TAG, "Skipping subtitle sample with no duration.");
|
||||
} else {
|
||||
setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data);
|
||||
setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data);
|
||||
// Note: If we ever want to support DRM protected subtitles then we'll need to output the
|
||||
// appropriate encryption data here.
|
||||
track.output.sampleData(subtitleSample, subtitleSample.limit());
|
||||
|
|
@ -1829,10 +1829,8 @@ public class MatroskaExtractor implements Extractor {
|
|||
chunkSize += size;
|
||||
chunkOffset = offset; // The offset is to the end of the sample.
|
||||
if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {
|
||||
// We haven't read enough samples to output a chunk.
|
||||
return;
|
||||
outputPendingSampleMetadata(track);
|
||||
}
|
||||
outputPendingSampleMetadata(track);
|
||||
}
|
||||
|
||||
public void outputPendingSampleMetadata(Track track) {
|
||||
|
|
|
|||
|
|
@ -379,6 +379,9 @@ import java.util.List;
|
|||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_dfLa = 0x64664c61;
|
||||
|
||||
@SuppressWarnings("ConstantCaseForConstants")
|
||||
public static final int TYPE_twos = 0x74776f73;
|
||||
|
||||
public final int type;
|
||||
|
||||
public Atom(int type) {
|
||||
|
|
|
|||
|
|
@ -798,6 +798,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
|| childAtomType == Atom.TYPE_sawb
|
||||
|| childAtomType == Atom.TYPE_lpcm
|
||||
|| childAtomType == Atom.TYPE_sowt
|
||||
|| childAtomType == Atom.TYPE_twos
|
||||
|| childAtomType == Atom.TYPE__mp3
|
||||
|| childAtomType == Atom.TYPE_alac
|
||||
|| childAtomType == Atom.TYPE_alaw
|
||||
|
|
@ -1086,6 +1087,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
|
||||
int channelCount;
|
||||
int sampleRate;
|
||||
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
|
||||
|
||||
if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
|
||||
channelCount = parent.readUnsignedShort();
|
||||
|
|
@ -1147,6 +1149,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
mimeType = MimeTypes.AUDIO_AMR_WB;
|
||||
} else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
|
||||
mimeType = MimeTypes.AUDIO_RAW;
|
||||
pcmEncoding = C.ENCODING_PCM_16BIT;
|
||||
} else if (atomType == Atom.TYPE_twos) {
|
||||
mimeType = MimeTypes.AUDIO_RAW;
|
||||
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
|
||||
} else if (atomType == Atom.TYPE__mp3) {
|
||||
mimeType = MimeTypes.AUDIO_MPEG;
|
||||
} else if (atomType == Atom.TYPE_alac) {
|
||||
|
|
@ -1233,9 +1239,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||
}
|
||||
|
||||
if (out.format == null && mimeType != null) {
|
||||
// TODO: Determine the correct PCM encoding.
|
||||
@C.PcmEncoding int pcmEncoding =
|
||||
MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE;
|
||||
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
|
||||
initializationData == null ? null : Collections.singletonList(initializationData),
|
||||
|
|
|
|||
|
|
@ -168,7 +168,6 @@ public class FragmentedMp4Extractor implements Extractor {
|
|||
private int sampleBytesWritten;
|
||||
private int sampleCurrentNalBytesRemaining;
|
||||
private boolean processSeiNalUnitPayload;
|
||||
private boolean isAc4HeaderRequired;
|
||||
|
||||
// Extractor output.
|
||||
@MonotonicNonNull private ExtractorOutput extractorOutput;
|
||||
|
|
@ -302,7 +301,6 @@ public class FragmentedMp4Extractor implements Extractor {
|
|||
pendingMetadataSampleBytes = 0;
|
||||
pendingSeekTimeUs = timeUs;
|
||||
containerAtoms.clear();
|
||||
isAc4HeaderRequired = false;
|
||||
enterReadingAtomHeaderState();
|
||||
}
|
||||
|
||||
|
|
@ -1222,7 +1220,6 @@ public class FragmentedMp4Extractor implements Extractor {
|
|||
* @throws InterruptedException If the thread is interrupted.
|
||||
*/
|
||||
private boolean readSample(ExtractorInput input) throws IOException, InterruptedException {
|
||||
int outputSampleEncryptionDataSize = 0;
|
||||
if (parserState == STATE_READING_SAMPLE_START) {
|
||||
if (currentTrackBundle == null) {
|
||||
@Nullable TrackBundle currentTrackBundle = getNextFragmentRun(trackBundles);
|
||||
|
|
@ -1270,11 +1267,14 @@ public class FragmentedMp4Extractor implements Extractor {
|
|||
}
|
||||
sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData();
|
||||
sampleSize += sampleBytesWritten;
|
||||
outputSampleEncryptionDataSize = sampleBytesWritten;
|
||||
if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) {
|
||||
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
|
||||
currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
|
||||
sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
|
||||
sampleSize += Ac4Util.SAMPLE_HEADER_SIZE;
|
||||
}
|
||||
parserState = STATE_READING_SAMPLE_CONTINUE;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
isAc4HeaderRequired =
|
||||
MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType);
|
||||
}
|
||||
|
||||
TrackFragment fragment = currentTrackBundle.fragment;
|
||||
|
|
@ -1339,14 +1339,6 @@ public class FragmentedMp4Extractor implements Extractor {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if (isAc4HeaderRequired) {
|
||||
Ac4Util.getAc4SampleHeader(sampleSize - outputSampleEncryptionDataSize, scratch);
|
||||
int length = scratch.limit();
|
||||
output.sampleData(scratch, length);
|
||||
sampleSize += length;
|
||||
sampleBytesWritten += length;
|
||||
isAc4HeaderRequired = false;
|
||||
}
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);
|
||||
sampleBytesWritten += writtenBytes;
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
@Nullable private ParsableByteArray atomData;
|
||||
|
||||
private int sampleTrackIndex;
|
||||
private int sampleBytesRead;
|
||||
private int sampleBytesWritten;
|
||||
private int sampleCurrentNalBytesRemaining;
|
||||
private boolean isAc4HeaderRequired;
|
||||
|
||||
// Extractor outputs.
|
||||
@MonotonicNonNull private ExtractorOutput extractorOutput;
|
||||
|
|
@ -160,9 +160,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
containerAtoms.clear();
|
||||
atomHeaderBytesRead = 0;
|
||||
sampleTrackIndex = C.INDEX_UNSET;
|
||||
sampleBytesRead = 0;
|
||||
sampleBytesWritten = 0;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
isAc4HeaderRequired = false;
|
||||
if (position == 0) {
|
||||
enterReadingAtomHeaderState();
|
||||
} else if (tracks != null) {
|
||||
|
|
@ -507,15 +507,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
if (sampleTrackIndex == C.INDEX_UNSET) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
isAc4HeaderRequired =
|
||||
MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType);
|
||||
}
|
||||
Mp4Track track = tracks[sampleTrackIndex];
|
||||
TrackOutput trackOutput = track.trackOutput;
|
||||
int sampleIndex = track.sampleIndex;
|
||||
long position = track.sampleTable.offsets[sampleIndex];
|
||||
int sampleSize = track.sampleTable.sizes[sampleIndex];
|
||||
long skipAmount = position - inputPosition + sampleBytesWritten;
|
||||
long skipAmount = position - inputPosition + sampleBytesRead;
|
||||
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
|
||||
positionHolder.position = position;
|
||||
return RESULT_SEEK;
|
||||
|
|
@ -543,6 +541,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
if (sampleCurrentNalBytesRemaining == 0) {
|
||||
// Read the NAL length so that we know where we find the next one.
|
||||
input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
|
||||
sampleBytesRead += nalUnitLengthFieldLength;
|
||||
nalLength.setPosition(0);
|
||||
int nalLengthInt = nalLength.readInt();
|
||||
if (nalLengthInt < 0) {
|
||||
|
|
@ -557,21 +556,23 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
} else {
|
||||
// Write the payload of the NAL unit.
|
||||
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);
|
||||
sampleBytesRead += writtenBytes;
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isAc4HeaderRequired) {
|
||||
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
|
||||
int length = scratch.limit();
|
||||
trackOutput.sampleData(scratch, length);
|
||||
sampleSize += length;
|
||||
sampleBytesWritten += length;
|
||||
isAc4HeaderRequired = false;
|
||||
if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) {
|
||||
if (sampleBytesWritten == 0) {
|
||||
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
|
||||
trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
|
||||
sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
|
||||
}
|
||||
sampleSize += Ac4Util.SAMPLE_HEADER_SIZE;
|
||||
}
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
|
||||
sampleBytesRead += writtenBytes;
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
|
|
@ -580,6 +581,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
|
||||
track.sampleIndex++;
|
||||
sampleTrackIndex = C.INDEX_UNSET;
|
||||
sampleBytesRead = 0;
|
||||
sampleBytesWritten = 0;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
return RESULT_CONTINUE;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.Ac3Util;
|
||||
|
|
@ -23,11 +24,15 @@ import com.google.android.exoplayer2.audio.Ac3Util.SyncFrameInfo;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous (E-)AC-3 byte stream and extracts individual samples.
|
||||
|
|
@ -47,10 +52,10 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||
|
||||
private final ParsableBitArray headerScratchBits;
|
||||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
@Nullable private final String language;
|
||||
|
||||
private String trackFormatId;
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
|
||||
@State private int state;
|
||||
private int bytesRead;
|
||||
|
|
@ -60,7 +65,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||
|
||||
// Used when parsing the header.
|
||||
private long sampleDurationUs;
|
||||
private Format format;
|
||||
@MonotonicNonNull private Format format;
|
||||
private int sampleSize;
|
||||
|
||||
// Used when reading the samples.
|
||||
|
|
@ -78,7 +83,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||
*
|
||||
* @param language Track language.
|
||||
*/
|
||||
public Ac3Reader(String language) {
|
||||
public Ac3Reader(@Nullable String language) {
|
||||
headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]);
|
||||
headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
|
||||
state = STATE_FINDING_SYNC;
|
||||
|
|
@ -95,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
|
||||
generator.generateNewId();
|
||||
trackFormatId = generator.getFormatId();
|
||||
formatId = generator.getFormatId();
|
||||
output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +111,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_SYNC:
|
||||
|
|
@ -185,19 +191,28 @@ public final class Ac3Reader implements ElementaryStreamReader {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the sample header.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
/** Parses the sample header. */
|
||||
@RequiresNonNull("output")
|
||||
private void parseHeader() {
|
||||
headerScratchBits.setPosition(0);
|
||||
SyncFrameInfo frameInfo = Ac3Util.parseAc3SyncframeInfo(headerScratchBits);
|
||||
if (format == null || frameInfo.channelCount != format.channelCount
|
||||
if (format == null
|
||||
|| frameInfo.channelCount != format.channelCount
|
||||
|| frameInfo.sampleRate != format.sampleRate
|
||||
|| frameInfo.mimeType != format.sampleMimeType) {
|
||||
format = Format.createAudioSampleFormat(trackFormatId, frameInfo.mimeType, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, frameInfo.channelCount, frameInfo.sampleRate, null,
|
||||
null, 0, language);
|
||||
|| Util.areEqual(frameInfo.mimeType, format.sampleMimeType)) {
|
||||
format =
|
||||
Format.createAudioSampleFormat(
|
||||
formatId,
|
||||
frameInfo.mimeType,
|
||||
null,
|
||||
Format.NO_VALUE,
|
||||
Format.NO_VALUE,
|
||||
frameInfo.channelCount,
|
||||
frameInfo.sampleRate,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
language);
|
||||
output.format(format);
|
||||
}
|
||||
sampleSize = frameInfo.frameSize;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.Ac4Util;
|
||||
|
|
@ -23,12 +24,15 @@ import com.google.android.exoplayer2.audio.Ac4Util.SyncFrameInfo;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/** Parses a continuous AC-4 byte stream and extracts individual samples. */
|
||||
public final class Ac4Reader implements ElementaryStreamReader {
|
||||
|
|
@ -44,10 +48,10 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
|
||||
private final ParsableBitArray headerScratchBits;
|
||||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
@Nullable private final String language;
|
||||
|
||||
private String trackFormatId;
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
|
||||
@State private int state;
|
||||
private int bytesRead;
|
||||
|
|
@ -58,7 +62,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
|
||||
// Used when parsing the header.
|
||||
private long sampleDurationUs;
|
||||
private Format format;
|
||||
@MonotonicNonNull private Format format;
|
||||
private int sampleSize;
|
||||
|
||||
// Used when reading the samples.
|
||||
|
|
@ -74,7 +78,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
*
|
||||
* @param language Track language.
|
||||
*/
|
||||
public Ac4Reader(String language) {
|
||||
public Ac4Reader(@Nullable String language) {
|
||||
headerScratchBits = new ParsableBitArray(new byte[Ac4Util.HEADER_SIZE_FOR_PARSER]);
|
||||
headerScratchBytes = new ParsableByteArray(headerScratchBits.data);
|
||||
state = STATE_FINDING_SYNC;
|
||||
|
|
@ -95,7 +99,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) {
|
||||
generator.generateNewId();
|
||||
trackFormatId = generator.getFormatId();
|
||||
formatId = generator.getFormatId();
|
||||
output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +110,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_SYNC:
|
||||
|
|
@ -185,7 +190,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
}
|
||||
|
||||
/** Parses the sample header. */
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@RequiresNonNull("output")
|
||||
private void parseHeader() {
|
||||
headerScratchBits.setPosition(0);
|
||||
SyncFrameInfo frameInfo = Ac4Util.parseAc4SyncframeInfo(headerScratchBits);
|
||||
|
|
@ -195,7 +200,7 @@ public final class Ac4Reader implements ElementaryStreamReader {
|
|||
|| !MimeTypes.AUDIO_AC4.equals(format.sampleMimeType)) {
|
||||
format =
|
||||
Format.createAudioSampleFormat(
|
||||
trackFormatId,
|
||||
formatId,
|
||||
MimeTypes.AUDIO_AC4,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
|
|
@ -23,13 +24,18 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous ADTS byte stream and extracts individual frames.
|
||||
|
|
@ -62,11 +68,11 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
private final boolean exposeId3;
|
||||
private final ParsableBitArray adtsScratch;
|
||||
private final ParsableByteArray id3HeaderBuffer;
|
||||
private final String language;
|
||||
@Nullable private final String language;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
private TrackOutput id3Output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
@MonotonicNonNull private TrackOutput id3Output;
|
||||
|
||||
private int state;
|
||||
private int bytesRead;
|
||||
|
|
@ -90,7 +96,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
// Used when reading the samples.
|
||||
private long timeUs;
|
||||
|
||||
private TrackOutput currentOutput;
|
||||
@MonotonicNonNull private TrackOutput currentOutput;
|
||||
private long currentSampleDuration;
|
||||
|
||||
/**
|
||||
|
|
@ -104,7 +110,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
* @param exposeId3 True if the reader should expose ID3 information.
|
||||
* @param language Track language.
|
||||
*/
|
||||
public AdtsReader(boolean exposeId3, String language) {
|
||||
public AdtsReader(boolean exposeId3, @Nullable String language) {
|
||||
adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
|
||||
id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE));
|
||||
setFindingSampleState();
|
||||
|
|
@ -130,6 +136,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
idGenerator.generateNewId();
|
||||
formatId = idGenerator.getFormatId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
currentOutput = output;
|
||||
if (exposeId3) {
|
||||
idGenerator.generateNewId();
|
||||
id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
|
||||
|
|
@ -147,6 +154,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) throws ParserException {
|
||||
assertTracksCreated();
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_SAMPLE:
|
||||
|
|
@ -425,9 +433,8 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Id3 header.
|
||||
*/
|
||||
/** Parses the Id3 header. */
|
||||
@RequiresNonNull("id3Output")
|
||||
private void parseId3Header() {
|
||||
id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE);
|
||||
id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET);
|
||||
|
|
@ -435,9 +442,8 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the sample header.
|
||||
*/
|
||||
/** Parses the sample header. */
|
||||
@RequiresNonNull("output")
|
||||
private void parseAdtsHeader() throws ParserException {
|
||||
adtsScratch.setPosition(0);
|
||||
|
||||
|
|
@ -487,9 +493,8 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
setReadingSampleState(output, sampleDurationUs, 0, sampleSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the rest of the sample
|
||||
*/
|
||||
/** Reads the rest of the sample */
|
||||
@RequiresNonNull("currentOutput")
|
||||
private void readSample(ParsableByteArray data) {
|
||||
int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
|
||||
currentOutput.sampleData(data, bytesToRead);
|
||||
|
|
@ -501,4 +506,10 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
}
|
||||
}
|
||||
|
||||
@EnsuresNonNull({"output", "currentOutput", "id3Output"})
|
||||
private void assertTracksCreated() {
|
||||
Assertions.checkNotNull(output);
|
||||
Util.castNonNull(currentOutput);
|
||||
Util.castNonNull(id3Output);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||
import com.google.android.exoplayer2.text.cea.Cea708InitializationData;
|
||||
|
|
@ -134,6 +135,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||
return new SparseArray<>();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
|
||||
switch (streamType) {
|
||||
|
|
@ -247,7 +249,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||
// Skip reserved (8).
|
||||
scratchDescriptorData.skipBytes(1);
|
||||
|
||||
List<byte[]> initializationData = null;
|
||||
@Nullable List<byte[]> initializationData = null;
|
||||
// The wide_aspect_ratio flag only has meaning for CEA-708.
|
||||
if (isDigital) {
|
||||
boolean isWideAspectRatio = (flags & 0x40) != 0;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,17 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.DtsUtil;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous DTS byte stream and extracts individual samples.
|
||||
|
|
@ -35,10 +39,10 @@ public final class DtsReader implements ElementaryStreamReader {
|
|||
private static final int HEADER_SIZE = 18;
|
||||
|
||||
private final ParsableByteArray headerScratchBytes;
|
||||
private final String language;
|
||||
@Nullable private final String language;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
|
||||
private int state;
|
||||
private int bytesRead;
|
||||
|
|
@ -48,7 +52,7 @@ public final class DtsReader implements ElementaryStreamReader {
|
|||
|
||||
// Used when parsing the header.
|
||||
private long sampleDurationUs;
|
||||
private Format format;
|
||||
@MonotonicNonNull private Format format;
|
||||
private int sampleSize;
|
||||
|
||||
// Used when reading the samples.
|
||||
|
|
@ -59,7 +63,7 @@ public final class DtsReader implements ElementaryStreamReader {
|
|||
*
|
||||
* @param language Track language.
|
||||
*/
|
||||
public DtsReader(String language) {
|
||||
public DtsReader(@Nullable String language) {
|
||||
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
|
||||
state = STATE_FINDING_SYNC;
|
||||
this.language = language;
|
||||
|
|
@ -86,6 +90,7 @@ public final class DtsReader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_SYNC:
|
||||
|
|
@ -162,9 +167,8 @@ public final class DtsReader implements ElementaryStreamReader {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the sample header.
|
||||
*/
|
||||
/** Parses the sample header. */
|
||||
@RequiresNonNull("output")
|
||||
private void parseHeader() {
|
||||
byte[] frameData = headerScratchBytes.data;
|
||||
if (format == null) {
|
||||
|
|
|
|||
|
|
@ -64,12 +64,12 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
|
|||
Format.createImageSampleFormat(
|
||||
idGenerator.getFormatId(),
|
||||
MimeTypes.APPLICATION_DVBSUBS,
|
||||
null,
|
||||
/* codecs= */ null,
|
||||
Format.NO_VALUE,
|
||||
0,
|
||||
/* selectionFlags= */ 0,
|
||||
Collections.singletonList(subtitleInfo.initializationData),
|
||||
subtitleInfo.language,
|
||||
null));
|
||||
/* drmInitData= */ null));
|
||||
outputs[i] = output;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,20 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous H262 byte stream and extracts individual frames.
|
||||
|
|
@ -38,27 +42,27 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
private static final int START_GROUP = 0xB8;
|
||||
private static final int START_USER_DATA = 0xB2;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
|
||||
// Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4.
|
||||
private static final double[] FRAME_RATE_VALUES = new double[] {
|
||||
24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60};
|
||||
|
||||
@Nullable private final UserDataReader userDataReader;
|
||||
@Nullable private final ParsableByteArray userDataParsable;
|
||||
|
||||
// State that should be reset on seek.
|
||||
@Nullable private final NalUnitTargetBuffer userData;
|
||||
private final boolean[] prefixFlags;
|
||||
private final CsdBuffer csdBuffer;
|
||||
private long totalBytesWritten;
|
||||
private boolean startedFirstSample;
|
||||
|
||||
// State that should not be reset on seek.
|
||||
private boolean hasOutputFormat;
|
||||
private long frameDurationUs;
|
||||
|
||||
private final UserDataReader userDataReader;
|
||||
private final ParsableByteArray userDataParsable;
|
||||
|
||||
// State that should be reset on seek.
|
||||
private final boolean[] prefixFlags;
|
||||
private final CsdBuffer csdBuffer;
|
||||
private final NalUnitTargetBuffer userData;
|
||||
private long totalBytesWritten;
|
||||
private boolean startedFirstSample;
|
||||
|
||||
// Per packet state that gets reset at the start of each packet.
|
||||
private long pesTimeUs;
|
||||
|
||||
|
|
@ -72,7 +76,7 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
this(null);
|
||||
}
|
||||
|
||||
/* package */ H262Reader(UserDataReader userDataReader) {
|
||||
/* package */ H262Reader(@Nullable UserDataReader userDataReader) {
|
||||
this.userDataReader = userDataReader;
|
||||
prefixFlags = new boolean[4];
|
||||
csdBuffer = new CsdBuffer(128);
|
||||
|
|
@ -89,7 +93,7 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
public void seek() {
|
||||
NalUnitUtil.clearPrefixFlags(prefixFlags);
|
||||
csdBuffer.reset();
|
||||
if (userDataReader != null) {
|
||||
if (userData != null) {
|
||||
userData.reset();
|
||||
}
|
||||
totalBytesWritten = 0;
|
||||
|
|
@ -114,6 +118,7 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
int offset = data.getPosition();
|
||||
int limit = data.limit();
|
||||
byte[] dataArray = data.data;
|
||||
|
|
@ -130,7 +135,7 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
if (!hasOutputFormat) {
|
||||
csdBuffer.onData(dataArray, offset, limit);
|
||||
}
|
||||
if (userDataReader != null) {
|
||||
if (userData != null) {
|
||||
userData.appendToNalUnit(dataArray, offset, limit);
|
||||
}
|
||||
return;
|
||||
|
|
@ -157,7 +162,7 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
hasOutputFormat = true;
|
||||
}
|
||||
}
|
||||
if (userDataReader != null) {
|
||||
if (userData != null) {
|
||||
int bytesAlreadyPassed = 0;
|
||||
if (lengthToStartCode > 0) {
|
||||
userData.appendToNalUnit(dataArray, offset, startCodeOffset);
|
||||
|
|
@ -167,8 +172,8 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
|
||||
if (userData.endNalUnit(bytesAlreadyPassed)) {
|
||||
int unescapedLength = NalUnitUtil.unescapeStream(userData.nalData, userData.nalLength);
|
||||
userDataParsable.reset(userData.nalData, unescapedLength);
|
||||
userDataReader.consume(sampleTimeUs, userDataParsable);
|
||||
Util.castNonNull(userDataParsable).reset(userData.nalData, unescapedLength);
|
||||
Util.castNonNull(userDataReader).consume(sampleTimeUs, userDataParsable);
|
||||
}
|
||||
|
||||
if (startCodeValue == START_USER_DATA && data.data[startCodeOffset + 2] == 0x1) {
|
||||
|
|
@ -211,10 +216,10 @@ public final class H262Reader implements ElementaryStreamReader {
|
|||
*
|
||||
* @param csdBuffer The csd buffer.
|
||||
* @param formatId The id for the generated format. May be null.
|
||||
* @return A pair consisting of the {@link Format} and the frame duration in microseconds, or
|
||||
* 0 if the duration could not be determined.
|
||||
* @return A pair consisting of the {@link Format} and the frame duration in microseconds, or 0 if
|
||||
* the duration could not be determined.
|
||||
*/
|
||||
private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, String formatId) {
|
||||
private static Pair<Format, Long> parseCsdBuffer(CsdBuffer csdBuffer, @Nullable String formatId) {
|
||||
byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length);
|
||||
|
||||
int firstByte = csdData[4] & 0xFF;
|
||||
|
|
|
|||
|
|
@ -23,15 +23,21 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous H264 byte stream and extracts individual frames.
|
||||
|
|
@ -51,9 +57,9 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
private long totalBytesWritten;
|
||||
private final boolean[] prefixFlags;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
private SampleReader sampleReader;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
@MonotonicNonNull private SampleReader sampleReader;
|
||||
|
||||
// State that should not be reset on seek.
|
||||
private boolean hasOutputFormat;
|
||||
|
|
@ -87,13 +93,15 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void seek() {
|
||||
totalBytesWritten = 0;
|
||||
randomAccessIndicator = false;
|
||||
NalUnitUtil.clearPrefixFlags(prefixFlags);
|
||||
sps.reset();
|
||||
pps.reset();
|
||||
sei.reset();
|
||||
sampleReader.reset();
|
||||
totalBytesWritten = 0;
|
||||
randomAccessIndicator = false;
|
||||
if (sampleReader != null) {
|
||||
sampleReader.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -113,6 +121,8 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
assertTracksCreated();
|
||||
|
||||
int offset = data.getPosition();
|
||||
int limit = data.limit();
|
||||
byte[] dataArray = data.data;
|
||||
|
|
@ -159,6 +169,7 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
@RequiresNonNull("sampleReader")
|
||||
private void startNalUnit(long position, int nalUnitType, long pesTimeUs) {
|
||||
if (!hasOutputFormat || sampleReader.needsSpsPps()) {
|
||||
sps.startNalUnit(nalUnitType);
|
||||
|
|
@ -168,6 +179,7 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
sampleReader.startNalUnit(position, nalUnitType, pesTimeUs);
|
||||
}
|
||||
|
||||
@RequiresNonNull("sampleReader")
|
||||
private void nalUnitData(byte[] dataArray, int offset, int limit) {
|
||||
if (!hasOutputFormat || sampleReader.needsSpsPps()) {
|
||||
sps.appendToNalUnit(dataArray, offset, limit);
|
||||
|
|
@ -177,6 +189,7 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
sampleReader.appendToNalUnit(dataArray, offset, limit);
|
||||
}
|
||||
|
||||
@RequiresNonNull({"output", "sampleReader"})
|
||||
private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
|
||||
if (!hasOutputFormat || sampleReader.needsSpsPps()) {
|
||||
sps.endNalUnit(discardPadding);
|
||||
|
|
@ -237,6 +250,12 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
}
|
||||
}
|
||||
|
||||
@EnsuresNonNull({"output", "sampleReader"})
|
||||
private void assertTracksCreated() {
|
||||
Assertions.checkStateNotNull(output);
|
||||
Util.castNonNull(sampleReader);
|
||||
}
|
||||
|
||||
/** Consumes a stream of NAL units and outputs samples. */
|
||||
private static final class SampleReader {
|
||||
|
||||
|
|
@ -478,7 +497,7 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
private boolean isComplete;
|
||||
private boolean hasSliceType;
|
||||
|
||||
private SpsData spsData;
|
||||
@Nullable private SpsData spsData;
|
||||
private int nalRefIdc;
|
||||
private int sliceType;
|
||||
private int frameNum;
|
||||
|
|
@ -542,6 +561,8 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
|
||||
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
|
||||
// See ISO 14496-10 subsection 7.4.1.2.4.
|
||||
SpsData spsData = Assertions.checkStateNotNull(this.spsData);
|
||||
SpsData otherSpsData = Assertions.checkStateNotNull(other.spsData);
|
||||
return isComplete
|
||||
&& (!other.isComplete
|
||||
|| frameNum != other.frameNum
|
||||
|
|
@ -552,15 +573,15 @@ public final class H264Reader implements ElementaryStreamReader {
|
|||
&& bottomFieldFlag != other.bottomFieldFlag)
|
||||
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|
||||
|| (spsData.picOrderCountType == 0
|
||||
&& other.spsData.picOrderCountType == 0
|
||||
&& otherSpsData.picOrderCountType == 0
|
||||
&& (picOrderCntLsb != other.picOrderCntLsb
|
||||
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|
||||
|| (spsData.picOrderCountType == 1
|
||||
&& other.spsData.picOrderCountType == 1
|
||||
&& otherSpsData.picOrderCountType == 1
|
||||
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|
||||
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|
||||
|| idrPicFlag != other.idrPicFlag
|
||||
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
|
||||
|| (idrPicFlag && idrPicId != other.idrPicId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,18 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous H.265 byte stream and extracts individual frames.
|
||||
|
|
@ -46,9 +52,9 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
|
||||
private final SeiReader seiReader;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
private SampleReader sampleReader;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
@MonotonicNonNull private SampleReader sampleReader;
|
||||
|
||||
// State that should not be reset on seek.
|
||||
private boolean hasOutputFormat;
|
||||
|
|
@ -84,14 +90,16 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void seek() {
|
||||
totalBytesWritten = 0;
|
||||
NalUnitUtil.clearPrefixFlags(prefixFlags);
|
||||
vps.reset();
|
||||
sps.reset();
|
||||
pps.reset();
|
||||
prefixSei.reset();
|
||||
suffixSei.reset();
|
||||
sampleReader.reset();
|
||||
totalBytesWritten = 0;
|
||||
if (sampleReader != null) {
|
||||
sampleReader.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -111,6 +119,8 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
assertTracksCreated();
|
||||
|
||||
while (data.bytesLeft() > 0) {
|
||||
int offset = data.getPosition();
|
||||
int limit = data.limit();
|
||||
|
|
@ -160,6 +170,7 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
@RequiresNonNull("sampleReader")
|
||||
private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
|
||||
if (hasOutputFormat) {
|
||||
sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);
|
||||
|
|
@ -172,6 +183,7 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
suffixSei.startNalUnit(nalUnitType);
|
||||
}
|
||||
|
||||
@RequiresNonNull("sampleReader")
|
||||
private void nalUnitData(byte[] dataArray, int offset, int limit) {
|
||||
if (hasOutputFormat) {
|
||||
sampleReader.readNalUnitData(dataArray, offset, limit);
|
||||
|
|
@ -184,6 +196,7 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
suffixSei.appendToNalUnit(dataArray, offset, limit);
|
||||
}
|
||||
|
||||
@RequiresNonNull({"output", "sampleReader"})
|
||||
private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
|
||||
if (hasOutputFormat) {
|
||||
sampleReader.endNalUnit(position, offset);
|
||||
|
|
@ -214,8 +227,11 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
}
|
||||
}
|
||||
|
||||
private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps,
|
||||
NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
|
||||
private static Format parseMediaFormat(
|
||||
@Nullable String formatId,
|
||||
NalUnitTargetBuffer vps,
|
||||
NalUnitTargetBuffer sps,
|
||||
NalUnitTargetBuffer pps) {
|
||||
// Build codec-specific data.
|
||||
byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength];
|
||||
System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength);
|
||||
|
|
@ -389,6 +405,12 @@ public final class H265Reader implements ElementaryStreamReader {
|
|||
}
|
||||
}
|
||||
|
||||
@EnsuresNonNull({"output", "sampleReader"})
|
||||
private void assertTracksCreated() {
|
||||
Assertions.checkStateNotNull(output);
|
||||
Util.castNonNull(sampleReader);
|
||||
}
|
||||
|
||||
private static final class SampleReader {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Parses ID3 data and extracts individual text information frames.
|
||||
|
|
@ -36,7 +38,7 @@ public final class Id3Reader implements ElementaryStreamReader {
|
|||
|
||||
private final ParsableByteArray id3Header;
|
||||
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
|
||||
// State that should be reset on seek.
|
||||
private boolean writingSample;
|
||||
|
|
@ -76,6 +78,7 @@ public final class Id3Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
if (!writingSample) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -106,6 +109,7 @@ public final class Id3Reader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void packetFinished() {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,14 @@ import com.google.android.exoplayer2.ParserException;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses and extracts samples from an AAC/LATM elementary stream.
|
||||
|
|
@ -43,14 +46,14 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||
private static final int SYNC_BYTE_FIRST = 0x56;
|
||||
private static final int SYNC_BYTE_SECOND = 0xE0;
|
||||
|
||||
private final String language;
|
||||
@Nullable private final String language;
|
||||
private final ParsableByteArray sampleDataBuffer;
|
||||
private final ParsableBitArray sampleBitArray;
|
||||
|
||||
// Track output info.
|
||||
private TrackOutput output;
|
||||
private Format format;
|
||||
private String formatId;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
@MonotonicNonNull private Format format;
|
||||
|
||||
// Parser state info.
|
||||
private int state;
|
||||
|
|
@ -99,6 +102,7 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) throws ParserException {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
int bytesToRead;
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
|
|
@ -150,6 +154,7 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||
*
|
||||
* @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes.
|
||||
*/
|
||||
@RequiresNonNull("output")
|
||||
private void parseAudioMuxElement(ParsableBitArray data) throws ParserException {
|
||||
boolean useSameStreamMux = data.readBit();
|
||||
if (!useSameStreamMux) {
|
||||
|
|
@ -173,9 +178,8 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42.
|
||||
*/
|
||||
/** Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */
|
||||
@RequiresNonNull("output")
|
||||
private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException {
|
||||
int audioMuxVersion = data.readBits(1);
|
||||
audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;
|
||||
|
|
@ -198,9 +202,19 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||
data.setPosition(startPosition);
|
||||
byte[] initData = new byte[(readBits + 7) / 8];
|
||||
data.readBits(initData, 0, readBits);
|
||||
Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz,
|
||||
Collections.singletonList(initData), null, 0, language);
|
||||
Format format =
|
||||
Format.createAudioSampleFormat(
|
||||
formatId,
|
||||
MimeTypes.AUDIO_AAC,
|
||||
/* codecs= */ null,
|
||||
Format.NO_VALUE,
|
||||
Format.NO_VALUE,
|
||||
channelCount,
|
||||
sampleRateHz,
|
||||
Collections.singletonList(initData),
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
language);
|
||||
if (!format.equals(this.format)) {
|
||||
this.format = format;
|
||||
sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
|
||||
|
|
@ -280,6 +294,7 @@ public final class LatmReader implements ElementaryStreamReader {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresNonNull("output")
|
||||
private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) {
|
||||
// The start of sample data in
|
||||
int bitPosition = data.getPosition();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses a continuous MPEG Audio byte stream and extracts individual frames.
|
||||
|
|
@ -36,10 +40,10 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||
|
||||
private final ParsableByteArray headerScratch;
|
||||
private final MpegAudioHeader header;
|
||||
private final String language;
|
||||
@Nullable private final String language;
|
||||
|
||||
private String formatId;
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
@MonotonicNonNull private String formatId;
|
||||
|
||||
private int state;
|
||||
private int frameBytesRead;
|
||||
|
|
@ -59,7 +63,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||
this(null);
|
||||
}
|
||||
|
||||
public MpegAudioReader(String language) {
|
||||
public MpegAudioReader(@Nullable String language) {
|
||||
state = STATE_FINDING_HEADER;
|
||||
// The first byte of an MPEG Audio frame header is always 0xFF.
|
||||
headerScratch = new ParsableByteArray(4);
|
||||
|
|
@ -89,6 +93,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
Assertions.checkStateNotNull(output); // Asserts that createTracks has been called.
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_HEADER:
|
||||
|
|
@ -146,20 +151,21 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||
|
||||
/**
|
||||
* Attempts to read the remaining two bytes of the frame header.
|
||||
* <p>
|
||||
* If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},
|
||||
*
|
||||
* <p>If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME},
|
||||
* the media format is output if this has not previously occurred, the four header bytes are
|
||||
* output as sample data, and the position of the source is advanced to the byte that immediately
|
||||
* follows the header.
|
||||
* <p>
|
||||
* If a frame header is read in full but cannot be parsed then the state is changed to
|
||||
* {@link #STATE_READING_HEADER}.
|
||||
* <p>
|
||||
* If a frame header is not read in full then the position of the source is advanced to the limit,
|
||||
* and the method should be called again with the next source to continue the read.
|
||||
*
|
||||
* <p>If a frame header is read in full but cannot be parsed then the state is changed to {@link
|
||||
* #STATE_READING_HEADER}.
|
||||
*
|
||||
* <p>If a frame header is not read in full then the position of the source is advanced to the
|
||||
* limit, and the method should be called again with the next source to continue the read.
|
||||
*
|
||||
* @param source The source from which to read.
|
||||
*/
|
||||
@RequiresNonNull("output")
|
||||
private void readHeaderRemainder(ParsableByteArray source) {
|
||||
int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead);
|
||||
source.readBytes(headerScratch.data, frameBytesRead, bytesToRead);
|
||||
|
|
@ -195,16 +201,17 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||
|
||||
/**
|
||||
* Attempts to read the remainder of the frame.
|
||||
* <p>
|
||||
* If a frame is read in full then true is returned. The frame will have been output, and the
|
||||
*
|
||||
* <p>If a frame is read in full then true is returned. The frame will have been output, and the
|
||||
* position of the source will have been advanced to the byte that immediately follows the end of
|
||||
* the frame.
|
||||
* <p>
|
||||
* If a frame is not read in full then the position of the source will have been advanced to the
|
||||
* limit, and the method should be called again with the next source to continue the read.
|
||||
*
|
||||
* <p>If a frame is not read in full then the position of the source will have been advanced to
|
||||
* the limit, and the method should be called again with the next source to continue the read.
|
||||
*
|
||||
* @param source The source from which to read.
|
||||
*/
|
||||
@RequiresNonNull("output")
|
||||
private void readFrameRemainder(ParsableByteArray source) {
|
||||
int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead);
|
||||
output.sampleData(source, bytesToRead);
|
||||
|
|
@ -219,5 +226,4 @@ public final class MpegAudioReader implements ElementaryStreamReader {
|
|||
frameBytesRead = 0;
|
||||
state = STATE_FINDING_HEADER;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,17 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Parses PES packet data and extracts samples.
|
||||
|
|
@ -45,7 +49,7 @@ public final class PesReader implements TsPayloadReader {
|
|||
private int state;
|
||||
private int bytesRead;
|
||||
|
||||
private TimestampAdjuster timestampAdjuster;
|
||||
@MonotonicNonNull private TimestampAdjuster timestampAdjuster;
|
||||
private boolean ptsFlag;
|
||||
private boolean dtsFlag;
|
||||
private boolean seenFirstDts;
|
||||
|
|
@ -79,6 +83,8 @@ public final class PesReader implements TsPayloadReader {
|
|||
|
||||
@Override
|
||||
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
|
||||
Assertions.checkStateNotNull(timestampAdjuster); // Asserts init has been called.
|
||||
|
||||
if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_HEADER:
|
||||
|
|
@ -119,7 +125,7 @@ public final class PesReader implements TsPayloadReader {
|
|||
int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength);
|
||||
// Read as much of the extended header as we're interested in, and skip the rest.
|
||||
if (continueRead(data, pesScratch.data, readLength)
|
||||
&& continueRead(data, null, extendedHeaderLength)) {
|
||||
&& continueRead(data, /* target= */ null, extendedHeaderLength)) {
|
||||
parseHeaderExtension();
|
||||
flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
|
||||
reader.packetStarted(timeUs, flags);
|
||||
|
|
@ -162,7 +168,8 @@ public final class PesReader implements TsPayloadReader {
|
|||
* @param targetLength The target length of the read.
|
||||
* @return Whether the target length has been reached.
|
||||
*/
|
||||
private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
|
||||
private boolean continueRead(
|
||||
ParsableByteArray source, @Nullable byte[] target, int targetLength) {
|
||||
int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
|
||||
if (bytesToRead <= 0) {
|
||||
return true;
|
||||
|
|
@ -207,6 +214,7 @@ public final class PesReader implements TsPayloadReader {
|
|||
return true;
|
||||
}
|
||||
|
||||
@RequiresNonNull("timestampAdjuster")
|
||||
private void parseHeaderExtension() {
|
||||
pesScratch.setPosition(0);
|
||||
timeUs = C.TIME_UNSET;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
|
|
@ -25,10 +26,13 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
|||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* Extracts data from the MPEG-2 PS container format.
|
||||
|
|
@ -67,8 +71,8 @@ public final class PsExtractor implements Extractor {
|
|||
private long lastTrackPosition;
|
||||
|
||||
// Accessed only by the loading thread.
|
||||
private PsBinarySearchSeeker psBinarySearchSeeker;
|
||||
private ExtractorOutput output;
|
||||
@Nullable private PsBinarySearchSeeker psBinarySearchSeeker;
|
||||
@MonotonicNonNull private ExtractorOutput output;
|
||||
private boolean hasOutputSeekMap;
|
||||
|
||||
public PsExtractor() {
|
||||
|
|
@ -160,6 +164,7 @@ public final class PsExtractor implements Extractor {
|
|||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
Assertions.checkStateNotNull(output); // Asserts init has been called.
|
||||
|
||||
long inputLength = input.getLength();
|
||||
boolean canReadDuration = inputLength != C.LENGTH_UNSET;
|
||||
|
|
@ -221,7 +226,7 @@ public final class PsExtractor implements Extractor {
|
|||
PesReader payloadReader = psPayloadReaders.get(streamId);
|
||||
if (!foundAllTracks) {
|
||||
if (payloadReader == null) {
|
||||
ElementaryStreamReader elementaryStreamReader = null;
|
||||
@Nullable ElementaryStreamReader elementaryStreamReader = null;
|
||||
if (streamId == PRIVATE_STREAM_1) {
|
||||
// Private stream, used for AC3 audio.
|
||||
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
|
||||
|
|
@ -278,6 +283,7 @@ public final class PsExtractor implements Extractor {
|
|||
|
||||
// Internals.
|
||||
|
||||
@RequiresNonNull("output")
|
||||
private void maybeOutputSeekMap(long inputLength) {
|
||||
if (!hasOutputSeekMap) {
|
||||
hasOutputSeekMap = true;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
|
|
@ -45,7 +46,7 @@ public final class SeiReader {
|
|||
idGenerator.generateNewId();
|
||||
TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
|
||||
Format channelFormat = closedCaptionFormats.get(i);
|
||||
String channelMimeType = channelFormat.sampleMimeType;
|
||||
@Nullable String channelMimeType = channelFormat.sampleMimeType;
|
||||
Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
|
||||
|| MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
|
||||
"Invalid closed caption mime type provided: " + channelMimeType);
|
||||
|
|
@ -69,5 +70,4 @@ public final class SeiReader {
|
|||
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
|
||||
CeaUtil.consume(pesTimeUs, seiBuffer, outputs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,17 +19,21 @@ import com.google.android.exoplayer2.C;
|
|||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Parses splice info sections as defined by SCTE35.
|
||||
*/
|
||||
public final class SpliceInfoSectionReader implements SectionPayloadReader {
|
||||
|
||||
private TimestampAdjuster timestampAdjuster;
|
||||
private TrackOutput output;
|
||||
@MonotonicNonNull private TimestampAdjuster timestampAdjuster;
|
||||
@MonotonicNonNull private TrackOutput output;
|
||||
private boolean formatDeclared;
|
||||
|
||||
@Override
|
||||
|
|
@ -44,6 +48,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
|
|||
|
||||
@Override
|
||||
public void consume(ParsableByteArray sectionData) {
|
||||
assertInitialized();
|
||||
if (!formatDeclared) {
|
||||
if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
|
||||
// There is not enough information to initialize the timestamp adjuster.
|
||||
|
|
@ -59,4 +64,9 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
|
|||
sampleSize, 0, null);
|
||||
}
|
||||
|
||||
@EnsuresNonNull({"timestampAdjuster", "output"})
|
||||
private void assertInitialized() {
|
||||
Assertions.checkStateNotNull(timestampAdjuster);
|
||||
Util.castNonNull(output);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import android.util.SparseArray;
|
|||
import android.util.SparseBooleanArray;
|
||||
import android.util.SparseIntArray;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
|
|
@ -587,8 +588,11 @@ public final class TsExtractor implements Extractor {
|
|||
continue;
|
||||
}
|
||||
|
||||
TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader
|
||||
: payloadReaderFactory.createPayloadReader(streamType, esInfo);
|
||||
@Nullable
|
||||
TsPayloadReader reader =
|
||||
mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3
|
||||
? id3Reader
|
||||
: payloadReaderFactory.createPayloadReader(streamType, esInfo);
|
||||
if (mode != MODE_HLS
|
||||
|| elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) {
|
||||
trackIdToPidScratch.put(trackId, elementaryPid);
|
||||
|
|
@ -602,7 +606,7 @@ public final class TsExtractor implements Extractor {
|
|||
int trackPid = trackIdToPidScratch.valueAt(i);
|
||||
trackIds.put(trackId, true);
|
||||
trackPids.put(trackPid, true);
|
||||
TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
|
||||
@Nullable TsPayloadReader reader = trackIdToReaderScratch.valueAt(i);
|
||||
if (reader != null) {
|
||||
if (reader != id3Reader) {
|
||||
reader.init(timestampAdjuster, output,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
|
|
@ -53,11 +54,11 @@ public interface TsPayloadReader {
|
|||
*
|
||||
* @param streamType Stream type value as defined in the PMT entry or associated descriptors.
|
||||
* @param esInfo Information associated to the elementary stream provided in the PMT.
|
||||
* @return A {@link TsPayloadReader} for the packet stream carried by the provided pid.
|
||||
* @return A {@link TsPayloadReader} for the packet stream carried by the provided pid, or
|
||||
* {@code null} if the stream is not supported.
|
||||
*/
|
||||
@Nullable
|
||||
TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,18 +67,21 @@ public interface TsPayloadReader {
|
|||
final class EsInfo {
|
||||
|
||||
public final int streamType;
|
||||
public final String language;
|
||||
@Nullable public final String language;
|
||||
public final List<DvbSubtitleInfo> dvbSubtitleInfos;
|
||||
public final byte[] descriptorBytes;
|
||||
|
||||
/**
|
||||
* @param streamType The type of the stream as defined by the
|
||||
* {@link TsExtractor}{@code .TS_STREAM_TYPE_*}.
|
||||
* @param streamType The type of the stream as defined by the {@link TsExtractor}{@code
|
||||
* .TS_STREAM_TYPE_*}.
|
||||
* @param language The language of the stream, as defined by ISO/IEC 13818-1, section 2.6.18.
|
||||
* @param dvbSubtitleInfos Information about DVB subtitles associated to the stream.
|
||||
* @param descriptorBytes The descriptor bytes associated to the stream.
|
||||
*/
|
||||
public EsInfo(int streamType, String language, List<DvbSubtitleInfo> dvbSubtitleInfos,
|
||||
public EsInfo(
|
||||
int streamType,
|
||||
@Nullable String language,
|
||||
@Nullable List<DvbSubtitleInfo> dvbSubtitleInfos,
|
||||
byte[] descriptorBytes) {
|
||||
this.streamType = streamType;
|
||||
this.language = language;
|
||||
|
|
@ -134,6 +138,7 @@ public interface TsPayloadReader {
|
|||
this.firstTrackId = firstTrackId;
|
||||
this.trackIdIncrement = trackIdIncrement;
|
||||
trackId = ID_UNSET;
|
||||
formatId = "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
|
|
@ -44,7 +45,7 @@ import java.util.List;
|
|||
idGenerator.generateNewId();
|
||||
TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
|
||||
Format channelFormat = closedCaptionFormats.get(i);
|
||||
String channelMimeType = channelFormat.sampleMimeType;
|
||||
@Nullable String channelMimeType = channelFormat.sampleMimeType;
|
||||
Assertions.checkArgument(
|
||||
MimeTypes.APPLICATION_CEA608.equals(channelMimeType)
|
||||
|| MimeTypes.APPLICATION_CEA708.equals(channelMimeType),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -28,8 +28,11 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
|
|||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Extracts data from WAV byte streams.
|
||||
|
|
@ -46,9 +49,9 @@ public final class WavExtractor implements Extractor {
|
|||
/** Factory for {@link WavExtractor} instances. */
|
||||
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()};
|
||||
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput trackOutput;
|
||||
private OutputWriter outputWriter;
|
||||
@MonotonicNonNull private ExtractorOutput extractorOutput;
|
||||
@MonotonicNonNull private TrackOutput trackOutput;
|
||||
@MonotonicNonNull private OutputWriter outputWriter;
|
||||
private int dataStartPosition;
|
||||
private long dataEndPosition;
|
||||
|
||||
|
|
@ -84,6 +87,7 @@ public final class WavExtractor implements Extractor {
|
|||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
assertInitialized();
|
||||
if (outputWriter == null) {
|
||||
WavHeader header = WavHeaderReader.peek(input);
|
||||
if (header == null) {
|
||||
|
|
@ -91,12 +95,34 @@ public final class WavExtractor implements Extractor {
|
|||
throw new ParserException("Unsupported or unrecognized wav header.");
|
||||
}
|
||||
|
||||
@C.PcmEncoding
|
||||
int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample);
|
||||
if (pcmEncoding == C.ENCODING_INVALID) {
|
||||
throw new ParserException("Unsupported WAV format type: " + header.formatType);
|
||||
if (header.formatType == WavUtil.TYPE_IMA_ADPCM) {
|
||||
outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header);
|
||||
} else if (header.formatType == WavUtil.TYPE_ALAW) {
|
||||
outputWriter =
|
||||
new PassthroughOutputWriter(
|
||||
extractorOutput,
|
||||
trackOutput,
|
||||
header,
|
||||
MimeTypes.AUDIO_ALAW,
|
||||
/* pcmEncoding= */ Format.NO_VALUE);
|
||||
} else if (header.formatType == WavUtil.TYPE_MLAW) {
|
||||
outputWriter =
|
||||
new PassthroughOutputWriter(
|
||||
extractorOutput,
|
||||
trackOutput,
|
||||
header,
|
||||
MimeTypes.AUDIO_MLAW,
|
||||
/* pcmEncoding= */ Format.NO_VALUE);
|
||||
} else {
|
||||
@C.PcmEncoding
|
||||
int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample);
|
||||
if (pcmEncoding == C.ENCODING_INVALID) {
|
||||
throw new ParserException("Unsupported WAV format type: " + header.formatType);
|
||||
}
|
||||
outputWriter =
|
||||
new PassthroughOutputWriter(
|
||||
extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding);
|
||||
}
|
||||
outputWriter = new PcmOutputWriter(extractorOutput, trackOutput, header, pcmEncoding);
|
||||
}
|
||||
|
||||
if (dataStartPosition == C.POSITION_UNSET) {
|
||||
|
|
@ -113,6 +139,12 @@ public final class WavExtractor implements Extractor {
|
|||
return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
@EnsuresNonNull({"extractorOutput", "trackOutput"})
|
||||
private void assertInitialized() {
|
||||
Assertions.checkStateNotNull(trackOutput);
|
||||
Util.castNonNull(extractorOutput);
|
||||
}
|
||||
|
||||
/** Writes to the extractor's output. */
|
||||
private interface OutputWriter {
|
||||
|
||||
|
|
@ -150,61 +182,56 @@ public final class WavExtractor implements Extractor {
|
|||
throws IOException, InterruptedException;
|
||||
}
|
||||
|
||||
private static final class PcmOutputWriter implements OutputWriter {
|
||||
private static final class PassthroughOutputWriter implements OutputWriter {
|
||||
|
||||
private final ExtractorOutput extractorOutput;
|
||||
private final TrackOutput trackOutput;
|
||||
private final WavHeader header;
|
||||
private final @C.PcmEncoding int pcmEncoding;
|
||||
private final int targetSampleSize;
|
||||
private final Format format;
|
||||
/** The target size of each output sample, in bytes. */
|
||||
private final int targetSampleSizeBytes;
|
||||
|
||||
/** The time at which the writer was last {@link #reset}. */
|
||||
private long startTimeUs;
|
||||
/**
|
||||
* The number of bytes that have been written to {@link #trackOutput} but have yet to be
|
||||
* included as part of a sample (i.e. the corresponding call to {@link
|
||||
* TrackOutput#sampleMetadata} has yet to be made).
|
||||
*/
|
||||
private int pendingOutputBytes;
|
||||
/**
|
||||
* The total number of frames in samples that have been written to the trackOutput since the
|
||||
* last call to {@link #reset}.
|
||||
*/
|
||||
private long outputFrameCount;
|
||||
private int pendingBytes;
|
||||
|
||||
public PcmOutputWriter(
|
||||
public PassthroughOutputWriter(
|
||||
ExtractorOutput extractorOutput,
|
||||
TrackOutput trackOutput,
|
||||
WavHeader header,
|
||||
@C.PcmEncoding int pcmEncoding) {
|
||||
String mimeType,
|
||||
@C.PcmEncoding int pcmEncoding)
|
||||
throws ParserException {
|
||||
this.extractorOutput = extractorOutput;
|
||||
this.trackOutput = trackOutput;
|
||||
this.header = header;
|
||||
this.pcmEncoding = pcmEncoding;
|
||||
// For PCM blocks correspond to single frames. This is validated in init(int, long).
|
||||
int bytesPerFrame = header.blockSize;
|
||||
targetSampleSize =
|
||||
Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(long timeUs) {
|
||||
startTimeUs = timeUs;
|
||||
outputFrameCount = 0;
|
||||
pendingBytes = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(int dataStartPosition, long dataEndPosition) throws ParserException {
|
||||
// Validate the header.
|
||||
int bytesPerFrame = header.numChannels * header.bitsPerSample / 8;
|
||||
// Validate the header. Blocks are expected to correspond to single frames.
|
||||
if (header.blockSize != bytesPerFrame) {
|
||||
throw new ParserException(
|
||||
"Expected block size: " + bytesPerFrame + "; got: " + header.blockSize);
|
||||
}
|
||||
|
||||
// Output the seek map.
|
||||
extractorOutput.seekMap(
|
||||
new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
|
||||
|
||||
// Output the format.
|
||||
Format format =
|
||||
targetSampleSizeBytes =
|
||||
Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND);
|
||||
format =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
mimeType,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ header.averageBytesPerSecond * 8,
|
||||
targetSampleSize,
|
||||
/* bitrate= */ header.frameRateHz * bytesPerFrame * 8,
|
||||
/* maxInputSize= */ targetSampleSizeBytes,
|
||||
header.numChannels,
|
||||
header.frameRateHz,
|
||||
pcmEncoding,
|
||||
|
|
@ -212,6 +239,19 @@ public final class WavExtractor implements Extractor {
|
|||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(long timeUs) {
|
||||
startTimeUs = timeUs;
|
||||
pendingOutputBytes = 0;
|
||||
outputFrameCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(int dataStartPosition, long dataEndPosition) {
|
||||
extractorOutput.seekMap(
|
||||
new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition));
|
||||
trackOutput.format(format);
|
||||
}
|
||||
|
||||
|
|
@ -220,34 +260,303 @@ public final class WavExtractor implements Extractor {
|
|||
throws IOException, InterruptedException {
|
||||
// Write sample data until we've reached the target sample size, or the end of the data.
|
||||
boolean endOfSampleData = bytesLeft == 0;
|
||||
while (!endOfSampleData && pendingBytes < targetSampleSize) {
|
||||
int bytesToRead = (int) Math.min(targetSampleSize - pendingBytes, bytesLeft);
|
||||
while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) {
|
||||
int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft);
|
||||
int bytesAppended = trackOutput.sampleData(input, bytesToRead, true);
|
||||
if (bytesAppended == RESULT_END_OF_INPUT) {
|
||||
endOfSampleData = true;
|
||||
} else {
|
||||
pendingBytes += bytesAppended;
|
||||
pendingOutputBytes += bytesAppended;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the corresponding sample metadata. Samples must be a whole number of frames. It's
|
||||
// possible pendingBytes is not a whole number of frames if the stream ended unexpectedly.
|
||||
// possible that the number of pending output bytes is not a whole number of frames if the
|
||||
// stream ended unexpectedly.
|
||||
int bytesPerFrame = header.blockSize;
|
||||
int pendingFrames = pendingBytes / bytesPerFrame;
|
||||
int pendingFrames = pendingOutputBytes / bytesPerFrame;
|
||||
if (pendingFrames > 0) {
|
||||
long timeUs =
|
||||
startTimeUs
|
||||
+ Util.scaleLargeTimestamp(
|
||||
outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
|
||||
int size = pendingFrames * bytesPerFrame;
|
||||
int offset = pendingBytes - size;
|
||||
int offset = pendingOutputBytes - size;
|
||||
trackOutput.sampleMetadata(
|
||||
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null);
|
||||
outputFrameCount += pendingFrames;
|
||||
pendingBytes = offset;
|
||||
pendingOutputBytes = offset;
|
||||
}
|
||||
|
||||
return endOfSampleData;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ImaAdPcmOutputWriter implements OutputWriter {
|
||||
|
||||
private static final int[] INDEX_TABLE = {
|
||||
-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8
|
||||
};
|
||||
|
||||
private static final int[] STEP_TABLE = {
|
||||
7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66,
|
||||
73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408,
|
||||
449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
|
||||
2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630,
|
||||
9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
|
||||
32767
|
||||
};
|
||||
|
||||
private final ExtractorOutput extractorOutput;
|
||||
private final TrackOutput trackOutput;
|
||||
private final WavHeader header;
|
||||
|
||||
/** Number of frames per block of the input (yet to be decoded) data. */
|
||||
private final int framesPerBlock;
|
||||
/** Target for the input (yet to be decoded) data. */
|
||||
private final byte[] inputData;
|
||||
/** Target for decoded (yet to be output) data. */
|
||||
private final ParsableByteArray decodedData;
|
||||
/** The target size of each output sample, in frames. */
|
||||
private final int targetSampleSizeFrames;
|
||||
/** The output format. */
|
||||
private final Format format;
|
||||
|
||||
/** The number of pending bytes in {@link #inputData}. */
|
||||
private int pendingInputBytes;
|
||||
/** The time at which the writer was last {@link #reset}. */
|
||||
private long startTimeUs;
|
||||
/**
|
||||
* The number of bytes that have been written to {@link #trackOutput} but have yet to be
|
||||
* included as part of a sample (i.e. the corresponding call to {@link
|
||||
* TrackOutput#sampleMetadata} has yet to be made).
|
||||
*/
|
||||
private int pendingOutputBytes;
|
||||
/**
|
||||
* The total number of frames in samples that have been written to the trackOutput since the
|
||||
* last call to {@link #reset}.
|
||||
*/
|
||||
private long outputFrameCount;
|
||||
|
||||
public ImaAdPcmOutputWriter(
|
||||
ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header)
|
||||
throws ParserException {
|
||||
this.extractorOutput = extractorOutput;
|
||||
this.trackOutput = trackOutput;
|
||||
this.header = header;
|
||||
targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND);
|
||||
|
||||
ParsableByteArray scratch = new ParsableByteArray(header.extraData);
|
||||
scratch.readLittleEndianUnsignedShort();
|
||||
framesPerBlock = scratch.readLittleEndianUnsignedShort();
|
||||
|
||||
int numChannels = header.numChannels;
|
||||
// Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update
|
||||
// - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI
|
||||
// ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter.
|
||||
int expectedFramesPerBlock =
|
||||
(((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1;
|
||||
if (framesPerBlock != expectedFramesPerBlock) {
|
||||
throw new ParserException(
|
||||
"Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock);
|
||||
}
|
||||
|
||||
// Calculate the number of blocks we'll need to decode to obtain an output sample of the
|
||||
// target sample size, and allocate suitably sized buffers for input and decoded data.
|
||||
int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock);
|
||||
inputData = new byte[maxBlocksToDecode * header.blockSize];
|
||||
decodedData =
|
||||
new ParsableByteArray(
|
||||
maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels));
|
||||
|
||||
// Create the format. We calculate the bitrate of the data before decoding, since this is the
|
||||
// bitrate of the stream itself.
|
||||
int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock;
|
||||
format =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
bitrate,
|
||||
/* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels),
|
||||
header.numChannels,
|
||||
header.frameRateHz,
|
||||
C.ENCODING_PCM_16BIT,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(long timeUs) {
|
||||
pendingInputBytes = 0;
|
||||
startTimeUs = timeUs;
|
||||
pendingOutputBytes = 0;
|
||||
outputFrameCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(int dataStartPosition, long dataEndPosition) {
|
||||
extractorOutput.seekMap(
|
||||
new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition));
|
||||
trackOutput.format(format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sampleData(ExtractorInput input, long bytesLeft)
|
||||
throws IOException, InterruptedException {
|
||||
// Calculate the number of additional frames that we need on the output side to complete a
|
||||
// sample of the target size.
|
||||
int targetFramesRemaining =
|
||||
targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes);
|
||||
// Calculate the whole number of blocks that we need to decode to obtain this many frames.
|
||||
int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock);
|
||||
int targetReadBytes = blocksToDecode * header.blockSize;
|
||||
|
||||
// Read input data until we've reached the target number of blocks, or the end of the data.
|
||||
boolean endOfSampleData = bytesLeft == 0;
|
||||
while (!endOfSampleData && pendingInputBytes < targetReadBytes) {
|
||||
int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft);
|
||||
int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead);
|
||||
if (bytesAppended == RESULT_END_OF_INPUT) {
|
||||
endOfSampleData = true;
|
||||
} else {
|
||||
pendingInputBytes += bytesAppended;
|
||||
}
|
||||
}
|
||||
|
||||
int pendingBlockCount = pendingInputBytes / header.blockSize;
|
||||
if (pendingBlockCount > 0) {
|
||||
// We have at least one whole block to decode.
|
||||
decode(inputData, pendingBlockCount, decodedData);
|
||||
pendingInputBytes -= pendingBlockCount * header.blockSize;
|
||||
|
||||
// Write all of the decoded data to the track output.
|
||||
int decodedDataSize = decodedData.limit();
|
||||
trackOutput.sampleData(decodedData, decodedDataSize);
|
||||
pendingOutputBytes += decodedDataSize;
|
||||
|
||||
// Output the next sample at the target size.
|
||||
int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes);
|
||||
if (pendingOutputFrames >= targetSampleSizeFrames) {
|
||||
writeSampleMetadata(targetSampleSizeFrames);
|
||||
}
|
||||
}
|
||||
|
||||
// If we've reached the end of the data, we might need to output a final partial sample.
|
||||
if (endOfSampleData) {
|
||||
int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes);
|
||||
if (pendingOutputFrames > 0) {
|
||||
writeSampleMetadata(pendingOutputFrames);
|
||||
}
|
||||
}
|
||||
|
||||
return endOfSampleData;
|
||||
}
|
||||
|
||||
private void writeSampleMetadata(int sampleFrames) {
|
||||
long timeUs =
|
||||
startTimeUs
|
||||
+ Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz);
|
||||
int size = numOutputFramesToBytes(sampleFrames);
|
||||
int offset = pendingOutputBytes - size;
|
||||
trackOutput.sampleMetadata(
|
||||
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null);
|
||||
outputFrameCount += sampleFrames;
|
||||
pendingOutputBytes -= size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes IMA ADPCM data to 16 bit PCM.
|
||||
*
|
||||
* @param input The input data to decode.
|
||||
* @param blockCount The number of blocks to decode.
|
||||
* @param output The output into which the decoded data will be written.
|
||||
*/
|
||||
private void decode(byte[] input, int blockCount, ParsableByteArray output) {
|
||||
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) {
|
||||
for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) {
|
||||
decodeBlockForChannel(input, blockIndex, channelIndex, output.data);
|
||||
}
|
||||
}
|
||||
int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount);
|
||||
output.reset(decodedDataSize);
|
||||
}
|
||||
|
||||
private void decodeBlockForChannel(
|
||||
byte[] input, int blockIndex, int channelIndex, byte[] output) {
|
||||
int blockSize = header.blockSize;
|
||||
int numChannels = header.numChannels;
|
||||
|
||||
// The input data consists for a four byte header [Ci] for each of the N channels, followed
|
||||
// by interleaved data segments [Ci-DATAj], each of which are four bytes long.
|
||||
//
|
||||
// [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc
|
||||
//
|
||||
// Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as
|
||||
// the number of data bytes for the channel in the block.
|
||||
int blockStartIndex = blockIndex * blockSize;
|
||||
int headerStartIndex = blockStartIndex + channelIndex * 4;
|
||||
int dataStartIndex = headerStartIndex + numChannels * 4;
|
||||
int dataSizeBytes = blockSize / numChannels - 4;
|
||||
|
||||
// Decode initialization. Casting to a short is necessary for the most significant bit to be
|
||||
// treated as -2^15 rather than 2^15.
|
||||
int predictedSample =
|
||||
(short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF));
|
||||
int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88);
|
||||
int step = STEP_TABLE[stepIndex];
|
||||
|
||||
// Output the initial 16 bit PCM sample from the header.
|
||||
int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2;
|
||||
output[outputIndex] = (byte) (predictedSample & 0xFF);
|
||||
output[outputIndex + 1] = (byte) (predictedSample >> 8);
|
||||
|
||||
// We examine each data byte twice during decode.
|
||||
for (int i = 0; i < dataSizeBytes * 2; i++) {
|
||||
int dataSegmentIndex = i / 8;
|
||||
int dataSegmentOffset = (i / 2) % 4;
|
||||
int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset;
|
||||
|
||||
int originalSample = input[dataIndex] & 0xFF;
|
||||
if (i % 2 == 0) {
|
||||
originalSample &= 0x0F; // Bottom four bits.
|
||||
} else {
|
||||
originalSample >>= 4; // Top four bits.
|
||||
}
|
||||
|
||||
int delta = originalSample & 0x07;
|
||||
int difference = ((2 * delta + 1) * step) >> 3;
|
||||
|
||||
if ((originalSample & 0x08) != 0) {
|
||||
difference = -difference;
|
||||
}
|
||||
|
||||
predictedSample += difference;
|
||||
predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767);
|
||||
|
||||
// Output the next 16 bit PCM sample to the correct position in the output.
|
||||
outputIndex += 2 * numChannels;
|
||||
output[outputIndex] = (byte) (predictedSample & 0xFF);
|
||||
output[outputIndex + 1] = (byte) (predictedSample >> 8);
|
||||
|
||||
stepIndex += INDEX_TABLE[originalSample];
|
||||
stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1);
|
||||
step = STEP_TABLE[stepIndex];
|
||||
}
|
||||
}
|
||||
|
||||
private int numOutputBytesToFrames(int bytes) {
|
||||
return bytes / (2 * header.numChannels);
|
||||
}
|
||||
|
||||
private int numOutputFramesToBytes(int frames) {
|
||||
return numOutputFramesToBytes(frames, header.numChannels);
|
||||
}
|
||||
|
||||
private static int numOutputFramesToBytes(int frames, int numChannels) {
|
||||
return frames * 2 * numChannels;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,20 +49,18 @@ import com.google.android.exoplayer2.util.Util;
|
|||
|
||||
@Override
|
||||
public SeekPoints getSeekPoints(long timeUs) {
|
||||
// Calculate the expected number of bytes of sample data corresponding to the requested time.
|
||||
long positionOffset = (timeUs * wavHeader.averageBytesPerSecond) / C.MICROS_PER_SECOND;
|
||||
// Calculate the containing block index, constraining to valid indices.
|
||||
long blockSize = wavHeader.blockSize;
|
||||
long blockIndex = Util.constrainValue(positionOffset / blockSize, 0, blockCount - 1);
|
||||
long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock);
|
||||
blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1);
|
||||
|
||||
long seekPosition = firstBlockPosition + (blockIndex * blockSize);
|
||||
long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize);
|
||||
long seekTimeUs = blockIndexToTimeUs(blockIndex);
|
||||
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
|
||||
if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) {
|
||||
return new SeekPoints(seekPoint);
|
||||
} else {
|
||||
long secondBlockIndex = blockIndex + 1;
|
||||
long secondSeekPosition = firstBlockPosition + (secondBlockIndex * blockSize);
|
||||
long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize);
|
||||
long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex);
|
||||
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
|
||||
return new SeekPoints(seekPoint, secondSeekPoint);
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||
*/
|
||||
@RequiresApi(21)
|
||||
/* package */ final class AsynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
private MediaCodecAsyncCallback mediaCodecAsyncCallback;
|
||||
private final MediaCodecAsyncCallback mediaCodecAsyncCallback;
|
||||
private final Handler handler;
|
||||
private final MediaCodec codec;
|
||||
@Nullable private IllegalStateException internalException;
|
||||
private boolean flushing;
|
||||
private Runnable onCodecStart;
|
||||
private Runnable codecStartRunnable;
|
||||
|
||||
/**
|
||||
* Create a new {@code AsynchronousMediaCodecAdapter}.
|
||||
|
|
@ -51,11 +51,16 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||
|
||||
@VisibleForTesting
|
||||
/* package */ AsynchronousMediaCodecAdapter(MediaCodec codec, Looper looper) {
|
||||
this.mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
|
||||
mediaCodecAsyncCallback = new MediaCodecAsyncCallback();
|
||||
handler = new Handler(looper);
|
||||
this.codec = codec;
|
||||
this.codec.setCallback(mediaCodecAsyncCallback);
|
||||
onCodecStart = () -> codec.start();
|
||||
codecStartRunnable = codec::start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
codecStartRunnable.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -105,7 +110,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||
flushing = false;
|
||||
mediaCodecAsyncCallback.flush();
|
||||
try {
|
||||
onCodecStart.run();
|
||||
codecStartRunnable.run();
|
||||
} catch (IllegalStateException e) {
|
||||
// Catch IllegalStateException directly so that we don't have to wrap it.
|
||||
internalException = e;
|
||||
|
|
@ -115,8 +120,8 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void setOnCodecStart(Runnable onCodecStart) {
|
||||
this.onCodecStart = onCodecStart;
|
||||
/* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
|
||||
this.codecStartRunnable = codecStartRunnable;
|
||||
}
|
||||
|
||||
private void maybeThrowException() throws IllegalStateException {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
@MonotonicNonNull private Handler handler;
|
||||
private long pendingFlushCount;
|
||||
private @State int state;
|
||||
private Runnable onCodecStart;
|
||||
private Runnable codecStartRunnable;
|
||||
@Nullable private IllegalStateException internalException;
|
||||
|
||||
/**
|
||||
|
|
@ -77,31 +76,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
this.codec = codec;
|
||||
this.handlerThread = handlerThread;
|
||||
state = STATE_CREATED;
|
||||
onCodecStart = codec::start;
|
||||
codecStartRunnable = codec::start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the operation of the instance.
|
||||
*
|
||||
* <p>After a call to this method, make sure to call {@link #shutdown()} to terminate the internal
|
||||
* Thread. You can only call this method once during the lifetime of this instance; calling this
|
||||
* method again will throw an {@link IllegalStateException}.
|
||||
*
|
||||
* @throws IllegalStateException If this method has been called already.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
Assertions.checkState(state == STATE_CREATED);
|
||||
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
codec.setCallback(this, handler);
|
||||
codecStartRunnable.run();
|
||||
state = STATE_STARTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int dequeueInputBufferIndex() {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
|
|
@ -112,8 +100,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
@Override
|
||||
public synchronized int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
|
|
@ -124,15 +110,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
@Override
|
||||
public synchronized MediaFormat getOutputFormat() {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
return mediaCodecAsyncCallback.getOutputFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void flush() {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
codec.flush();
|
||||
++pendingFlushCount;
|
||||
Util.castNonNull(handler).post(this::onFlushCompleted);
|
||||
|
|
@ -177,8 +159,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void setOnCodecStart(Runnable onCodecStart) {
|
||||
this.onCodecStart = onCodecStart;
|
||||
/* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
|
||||
this.codecStartRunnable = codecStartRunnable;
|
||||
}
|
||||
|
||||
private synchronized void onFlushCompleted() {
|
||||
|
|
@ -199,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
mediaCodecAsyncCallback.flush();
|
||||
try {
|
||||
onCodecStart.run();
|
||||
codecStartRunnable.run();
|
||||
} catch (IllegalStateException e) {
|
||||
internalException = e;
|
||||
} catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ import android.media.MediaFormat;
|
|||
*/
|
||||
/* package */ interface MediaCodecAdapter {
|
||||
|
||||
/**
|
||||
* Starts this instance.
|
||||
*
|
||||
* @see MediaCodec#start().
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Returns the next available input buffer index from the underlying {@link MediaCodec} or {@link
|
||||
* MediaCodec#INFO_TRY_AGAIN_LATER} if no such buffer exists.
|
||||
|
|
|
|||
|
|
@ -995,13 +995,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
} else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD
|
||||
&& Util.SDK_INT >= 23) {
|
||||
codecAdapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, getTrackType());
|
||||
((DedicatedThreadAsyncMediaCodecAdapter) codecAdapter).start();
|
||||
} else if (mediaCodecOperationMode == OPERATION_MODE_ASYNCHRONOUS_DEDICATED_THREAD_MULTI_LOCK
|
||||
&& Util.SDK_INT >= 23) {
|
||||
codecAdapter = new MultiLockAsyncMediaCodecAdapter(codec, getTrackType());
|
||||
((MultiLockAsyncMediaCodecAdapter) codecAdapter).start();
|
||||
} else {
|
||||
codecAdapter = new SynchronousMediaCodecAdapter(codec, getDequeueOutputBufferTimeoutUs());
|
||||
codecAdapter = new SynchronousMediaCodecAdapter(codec);
|
||||
}
|
||||
|
||||
TraceUtil.endSection();
|
||||
|
|
@ -1009,7 +1007,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
configureCodec(codecInfo, codec, inputFormat, crypto, codecOperatingRate);
|
||||
TraceUtil.endSection();
|
||||
TraceUtil.beginSection("startCodec");
|
||||
codec.start();
|
||||
codecAdapter.start();
|
||||
TraceUtil.endSection();
|
||||
codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||
getCodecBuffers(codec);
|
||||
|
|
@ -1460,15 +1458,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
&& SystemClock.elapsedRealtime() < codecHotswapDeadlineMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum time to block whilst waiting for a decoded output buffer.
|
||||
*
|
||||
* @return The maximum time to block, in microseconds.
|
||||
*/
|
||||
protected long getDequeueOutputBufferTimeoutUs() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate,
|
||||
* current {@link Format} and set of possible stream formats.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.IntArrayQueue;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayDeque;
|
||||
|
|
@ -94,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
private final HandlerThread handlerThread;
|
||||
@MonotonicNonNull private Handler handler;
|
||||
private Runnable onCodecStart;
|
||||
private Runnable codecStartRunnable;
|
||||
|
||||
/** Creates a new instance that wraps the specified {@link MediaCodec}. */
|
||||
/* package */ MultiLockAsyncMediaCodecAdapter(MediaCodec codec, int trackType) {
|
||||
|
|
@ -114,25 +113,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
codecException = null;
|
||||
state = STATE_CREATED;
|
||||
this.handlerThread = handlerThread;
|
||||
onCodecStart = codec::start;
|
||||
codecStartRunnable = codec::start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the operation of this instance.
|
||||
*
|
||||
* <p>After a call to this method, make sure to call {@link #shutdown()} to terminate the internal
|
||||
* Thread. You can only call this method once during the lifetime of an instance; calling this
|
||||
* method again will throw an {@link IllegalStateException}.
|
||||
*
|
||||
* @throws IllegalStateException If this method has been called already.
|
||||
*/
|
||||
@Override
|
||||
public void start() {
|
||||
synchronized (objectStateLock) {
|
||||
Assertions.checkState(state == STATE_CREATED);
|
||||
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
codec.setCallback(this, handler);
|
||||
codecStartRunnable.run();
|
||||
state = STATE_STARTED;
|
||||
}
|
||||
}
|
||||
|
|
@ -140,8 +130,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
@Override
|
||||
public int dequeueInputBufferIndex() {
|
||||
synchronized (objectStateLock) {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
|
|
@ -154,8 +142,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
@Override
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
synchronized (objectStateLock) {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
if (isFlushing()) {
|
||||
return MediaCodec.INFO_TRY_AGAIN_LATER;
|
||||
} else {
|
||||
|
|
@ -168,8 +154,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
@Override
|
||||
public MediaFormat getOutputFormat() {
|
||||
synchronized (objectStateLock) {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
if (currentFormat == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
|
@ -181,8 +165,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
@Override
|
||||
public void flush() {
|
||||
synchronized (objectStateLock) {
|
||||
Assertions.checkState(state == STATE_STARTED);
|
||||
|
||||
codec.flush();
|
||||
pendingFlush++;
|
||||
Util.castNonNull(handler).post(this::onFlushComplete);
|
||||
|
|
@ -200,8 +182,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ void setOnCodecStart(Runnable onCodecStart) {
|
||||
this.onCodecStart = onCodecStart;
|
||||
/* package */ void setCodecStartRunnable(Runnable codecStartRunnable) {
|
||||
this.codecStartRunnable = codecStartRunnable;
|
||||
}
|
||||
|
||||
private int dequeueAvailableInputBufferIndex() {
|
||||
|
|
@ -307,7 +289,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
clearAvailableOutput();
|
||||
codecException = null;
|
||||
try {
|
||||
onCodecStart.run();
|
||||
codecStartRunnable.run();
|
||||
} catch (IllegalStateException e) {
|
||||
codecException = e;
|
||||
} catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,16 @@ import android.media.MediaFormat;
|
|||
* A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode.
|
||||
*/
|
||||
/* package */ final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
private final MediaCodec codec;
|
||||
private final long dequeueOutputBufferTimeoutMs;
|
||||
|
||||
public SynchronousMediaCodecAdapter(MediaCodec mediaCodec, long dequeueOutputBufferTimeoutMs) {
|
||||
private final MediaCodec codec;
|
||||
|
||||
public SynchronousMediaCodecAdapter(MediaCodec mediaCodec) {
|
||||
this.codec = mediaCodec;
|
||||
this.dequeueOutputBufferTimeoutMs = dequeueOutputBufferTimeoutMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
codec.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -38,7 +42,7 @@ import android.media.MediaFormat;
|
|||
|
||||
@Override
|
||||
public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) {
|
||||
return codec.dequeueOutputBuffer(bufferInfo, dequeueOutputBufferTimeoutMs);
|
||||
return codec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -773,7 +773,7 @@ public final class DownloadHelper {
|
|||
}
|
||||
|
||||
// Initialization of array of Lists.
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private void onMediaPrepared() {
|
||||
Assertions.checkNotNull(mediaPreparer);
|
||||
Assertions.checkNotNull(mediaPreparer.mediaPeriods);
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ public final class Requirements implements Parcelable {
|
|||
private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) {
|
||||
// It's possible to query NetworkCapabilities from API level 23, but RequirementsWatcher only
|
||||
// fires an event to update its Requirements when NetworkCapabilities change from API level 24.
|
||||
// Since Requirements wont be updated, we assume connectivity is validated on API level 23.
|
||||
// Since Requirements won't be updated, we assume connectivity is validated on API level 23.
|
||||
if (Util.SDK_INT < 24) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -481,8 +481,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
*
|
||||
* @return The parsed object data.
|
||||
*/
|
||||
// incompatible types in argument.
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
private static ObjectData parseObjectData(ParsableBitArray data) {
|
||||
int objectId = data.readBits(16);
|
||||
data.skipBits(4); // Skip object_version_number
|
||||
|
|
@ -490,8 +488,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
boolean nonModifyingColorFlag = data.readBit();
|
||||
data.skipBits(1); // Skip reserved.
|
||||
|
||||
@Nullable byte[] topFieldData = null;
|
||||
@Nullable byte[] bottomFieldData = null;
|
||||
byte[] topFieldData = Util.EMPTY_BYTE_ARRAY;
|
||||
byte[] bottomFieldData = Util.EMPTY_BYTE_ARRAY;
|
||||
|
||||
if (objectCodingMethod == OBJECT_CODING_STRING) {
|
||||
int numberOfCodes = data.readBits(8);
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.text.ttml;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
|
|
@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan;
|
|||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import com.google.android.exoplayer2.text.SpanUtil;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
|
@ -77,32 +77,60 @@ import java.util.Map;
|
|||
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasFontColor()) {
|
||||
builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new ForegroundColorSpan(style.getFontColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasBackgroundColor()) {
|
||||
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new BackgroundColorSpan(style.getBackgroundColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.getFontFamily() != null) {
|
||||
builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new TypefaceSpan(style.getFontFamily()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.getTextAlign() != null) {
|
||||
builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new AlignmentSpan.Standard(style.getTextAlign()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
switch (style.getFontSizeUnit()) {
|
||||
case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
|
||||
builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new AbsoluteSizeSpan((int) style.getFontSize(), true),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.FONT_SIZE_UNIT_EM:
|
||||
builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new RelativeSizeSpan(style.getFontSize()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
|
||||
builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new RelativeSizeSpan(style.getFontSize() / 100),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.UNSPECIFIED:
|
||||
|
|
|
|||
|
|
@ -31,14 +31,19 @@ import java.util.regex.Pattern;
|
|||
*/
|
||||
/* package */ final class CssParser {
|
||||
|
||||
private static final String TAG = "CssParser";
|
||||
|
||||
private static final String RULE_START = "{";
|
||||
private static final String RULE_END = "}";
|
||||
private static final String PROPERTY_BGCOLOR = "background-color";
|
||||
private static final String PROPERTY_FONT_FAMILY = "font-family";
|
||||
private static final String PROPERTY_FONT_WEIGHT = "font-weight";
|
||||
private static final String PROPERTY_TEXT_COMBINE_UPRIGHT = "text-combine-upright";
|
||||
private static final String VALUE_ALL = "all";
|
||||
private static final String VALUE_DIGITS = "digits";
|
||||
private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
|
||||
private static final String VALUE_BOLD = "bold";
|
||||
private static final String VALUE_UNDERLINE = "underline";
|
||||
private static final String RULE_START = "{";
|
||||
private static final String RULE_END = "}";
|
||||
private static final String PROPERTY_FONT_STYLE = "font-style";
|
||||
private static final String VALUE_ITALIC = "italic";
|
||||
|
||||
|
|
@ -182,6 +187,8 @@ import java.util.regex.Pattern;
|
|||
style.setFontColor(ColorParser.parseCssColor(value));
|
||||
} else if (PROPERTY_BGCOLOR.equals(property)) {
|
||||
style.setBackgroundColor(ColorParser.parseCssColor(value));
|
||||
} else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) {
|
||||
style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS));
|
||||
} else if (PROPERTY_TEXT_DECORATION.equals(property)) {
|
||||
if (VALUE_UNDERLINE.equals(value)) {
|
||||
style.setUnderline(true);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ public final class WebvttCssStyle {
|
|||
@FontSizeUnit private int fontSizeUnit;
|
||||
private float fontSize;
|
||||
@Nullable private Layout.Alignment textAlign;
|
||||
private boolean combineUpright;
|
||||
|
||||
// Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed
|
||||
// because reset() only assigns fields, it doesn't read any.
|
||||
|
|
@ -118,6 +119,7 @@ public final class WebvttCssStyle {
|
|||
italic = UNSPECIFIED;
|
||||
fontSizeUnit = UNSPECIFIED;
|
||||
textAlign = null;
|
||||
combineUpright = false;
|
||||
}
|
||||
|
||||
public void setTargetId(String targetId) {
|
||||
|
|
@ -287,35 +289,12 @@ public final class WebvttCssStyle {
|
|||
return fontSize;
|
||||
}
|
||||
|
||||
public void cascadeFrom(WebvttCssStyle style) {
|
||||
if (style.hasFontColor) {
|
||||
setFontColor(style.fontColor);
|
||||
}
|
||||
if (style.bold != UNSPECIFIED) {
|
||||
bold = style.bold;
|
||||
}
|
||||
if (style.italic != UNSPECIFIED) {
|
||||
italic = style.italic;
|
||||
}
|
||||
if (style.fontFamily != null) {
|
||||
fontFamily = style.fontFamily;
|
||||
}
|
||||
if (linethrough == UNSPECIFIED) {
|
||||
linethrough = style.linethrough;
|
||||
}
|
||||
if (underline == UNSPECIFIED) {
|
||||
underline = style.underline;
|
||||
}
|
||||
if (textAlign == null) {
|
||||
textAlign = style.textAlign;
|
||||
}
|
||||
if (fontSizeUnit == UNSPECIFIED) {
|
||||
fontSizeUnit = style.fontSizeUnit;
|
||||
fontSize = style.fontSize;
|
||||
}
|
||||
if (style.hasBackgroundColor) {
|
||||
setBackgroundColor(style.backgroundColor);
|
||||
}
|
||||
public void setCombineUpright(boolean enabled) {
|
||||
this.combineUpright = enabled;
|
||||
}
|
||||
|
||||
public boolean getCombineUpright() {
|
||||
return combineUpright;
|
||||
}
|
||||
|
||||
private static int updateScoreForMatch(
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.text.webvtt;
|
||||
|
||||
import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.SpannedString;
|
||||
|
|
@ -37,6 +37,8 @@ import androidx.annotation.IntDef;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||
import com.google.android.exoplayer2.text.span.RubySpan;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -120,11 +122,13 @@ public final class WebvttCueParser {
|
|||
private static final String ENTITY_NON_BREAK_SPACE = "nbsp";
|
||||
|
||||
private static final String TAG_BOLD = "b";
|
||||
private static final String TAG_ITALIC = "i";
|
||||
private static final String TAG_UNDERLINE = "u";
|
||||
private static final String TAG_CLASS = "c";
|
||||
private static final String TAG_VOICE = "v";
|
||||
private static final String TAG_ITALIC = "i";
|
||||
private static final String TAG_LANG = "lang";
|
||||
private static final String TAG_RUBY = "ruby";
|
||||
private static final String TAG_RUBY_TEXT = "rt";
|
||||
private static final String TAG_UNDERLINE = "u";
|
||||
private static final String TAG_VOICE = "v";
|
||||
|
||||
private static final int STYLE_BOLD = Typeface.BOLD;
|
||||
private static final int STYLE_ITALIC = Typeface.ITALIC;
|
||||
|
|
@ -197,6 +201,7 @@ public final class WebvttCueParser {
|
|||
ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
|
||||
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
|
||||
int pos = 0;
|
||||
List<Element> nestedElements = new ArrayList<>();
|
||||
while (pos < markup.length()) {
|
||||
char curr = markup.charAt(pos);
|
||||
switch (curr) {
|
||||
|
|
@ -225,8 +230,14 @@ public final class WebvttCueParser {
|
|||
break;
|
||||
}
|
||||
startTag = startTagStack.pop();
|
||||
applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);
|
||||
} while(!startTag.name.equals(tagName));
|
||||
applySpansForTag(
|
||||
id, startTag, nestedElements, spannedText, styles, scratchStyleMatches);
|
||||
if (!startTagStack.isEmpty()) {
|
||||
nestedElements.add(new Element(startTag, spannedText.length()));
|
||||
} else {
|
||||
nestedElements.clear();
|
||||
}
|
||||
} while (!startTag.name.equals(tagName));
|
||||
} else if (!isVoidTag) {
|
||||
startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
|
||||
}
|
||||
|
|
@ -256,9 +267,15 @@ public final class WebvttCueParser {
|
|||
}
|
||||
// apply unclosed tags
|
||||
while (!startTagStack.isEmpty()) {
|
||||
applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);
|
||||
applySpansForTag(
|
||||
id, startTagStack.pop(), nestedElements, spannedText, styles, scratchStyleMatches);
|
||||
}
|
||||
applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
|
||||
applySpansForTag(
|
||||
id,
|
||||
StartTag.buildWholeCueVirtualTag(),
|
||||
/* nestedElements= */ Collections.emptyList(),
|
||||
spannedText,
|
||||
styles,
|
||||
scratchStyleMatches);
|
||||
return SpannedString.valueOf(spannedText);
|
||||
}
|
||||
|
|
@ -442,6 +459,8 @@ public final class WebvttCueParser {
|
|||
case TAG_CLASS:
|
||||
case TAG_ITALIC:
|
||||
case TAG_LANG:
|
||||
case TAG_RUBY:
|
||||
case TAG_RUBY_TEXT:
|
||||
case TAG_UNDERLINE:
|
||||
case TAG_VOICE:
|
||||
return true;
|
||||
|
|
@ -453,6 +472,7 @@ public final class WebvttCueParser {
|
|||
private static void applySpansForTag(
|
||||
@Nullable String cueId,
|
||||
StartTag startTag,
|
||||
List<Element> nestedElements,
|
||||
SpannableStringBuilder text,
|
||||
List<WebvttCssStyle> styles,
|
||||
List<StyleMatch> scratchStyleMatches) {
|
||||
|
|
@ -467,6 +487,29 @@ public final class WebvttCueParser {
|
|||
text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TAG_RUBY:
|
||||
@Nullable Element rubyTextElement = null;
|
||||
for (int i = 0; i < nestedElements.size(); i++) {
|
||||
if (TAG_RUBY_TEXT.equals(nestedElements.get(i).startTag.name)) {
|
||||
rubyTextElement = nestedElements.get(i);
|
||||
// Behaviour of multiple <rt> tags inside <ruby> is undefined, so use the first one.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rubyTextElement == null) {
|
||||
break;
|
||||
}
|
||||
// Move the rubyText from spannedText into the RubySpan.
|
||||
CharSequence rubyText =
|
||||
text.subSequence(rubyTextElement.startTag.position, rubyTextElement.endPosition);
|
||||
text.delete(rubyTextElement.startTag.position, rubyTextElement.endPosition);
|
||||
end -= rubyText.length();
|
||||
text.setSpan(
|
||||
new RubySpan(rubyText.toString(), RubySpan.POSITION_OVER),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TAG_UNDERLINE:
|
||||
text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
|
|
@ -492,7 +535,11 @@ public final class WebvttCueParser {
|
|||
return;
|
||||
}
|
||||
if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
|
||||
spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new StyleSpan(style.getStyle()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.isLinethrough()) {
|
||||
|
|
@ -502,39 +549,71 @@ public final class WebvttCueParser {
|
|||
spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasFontColor()) {
|
||||
spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new ForegroundColorSpan(style.getFontColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasBackgroundColor()) {
|
||||
spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new BackgroundColorSpan(style.getBackgroundColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.getFontFamily() != null) {
|
||||
spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new TypefaceSpan(style.getFontFamily()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
Layout.Alignment textAlign = style.getTextAlign();
|
||||
if (textAlign != null) {
|
||||
spannedText.setSpan(
|
||||
new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new AlignmentSpan.Standard(textAlign),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
switch (style.getFontSizeUnit()) {
|
||||
case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
|
||||
spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new AbsoluteSizeSpan((int) style.getFontSize(), true),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case WebvttCssStyle.FONT_SIZE_UNIT_EM:
|
||||
spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new RelativeSizeSpan(style.getFontSize()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
|
||||
spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new RelativeSizeSpan(style.getFontSize() / 100),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case WebvttCssStyle.UNSPECIFIED:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
if (style.getCombineUpright()) {
|
||||
spannedText.setSpan(
|
||||
new HorizontalTextInVerticalContextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -773,4 +852,19 @@ public final class WebvttCueParser {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/** Information about a complete element (i.e. start tag and end position). */
|
||||
private static class Element {
|
||||
private final StartTag startTag;
|
||||
/**
|
||||
* The position of the end of this element's text in the un-marked-up cue text (i.e. the
|
||||
* corollary to {@link StartTag#position}).
|
||||
*/
|
||||
private final int endPosition;
|
||||
|
||||
private Element(StartTag startTag, int endPosition) {
|
||||
this.startTag = startTag;
|
||||
this.endPosition = endPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,9 +203,10 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
|
|||
result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]);
|
||||
result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]);
|
||||
result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]);
|
||||
// Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate.
|
||||
// Assume default Wifi bitrate for Ethernet and 5G to prevent using the slower fallback.
|
||||
result.append(
|
||||
C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);
|
||||
result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,7 @@ public final class Util {
|
|||
public static boolean isEncodingLinearPcm(@C.Encoding int encoding) {
|
||||
return encoding == C.ENCODING_PCM_8BIT
|
||||
|| encoding == C.ENCODING_PCM_16BIT
|
||||
|| encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN
|
||||
|| encoding == C.ENCODING_PCM_24BIT
|
||||
|| encoding == C.ENCODING_PCM_32BIT
|
||||
|| encoding == C.ENCODING_PCM_FLOAT;
|
||||
|
|
@ -1423,14 +1424,13 @@ public final class Util {
|
|||
case C.ENCODING_PCM_8BIT:
|
||||
return channelCount;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
|
||||
return channelCount * 2;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
return channelCount * 3;
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
case C.ENCODING_PCM_FLOAT:
|
||||
return channelCount * 4;
|
||||
case C.ENCODING_PCM_A_LAW:
|
||||
case C.ENCODING_PCM_MU_LAW:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
|
|
@ -2126,6 +2126,8 @@ public final class Util {
|
|||
return C.NETWORK_TYPE_3G;
|
||||
case TelephonyManager.NETWORK_TYPE_LTE:
|
||||
return C.NETWORK_TYPE_4G;
|
||||
case TelephonyManager.NETWORK_TYPE_NR:
|
||||
return C.NETWORK_TYPE_5G;
|
||||
case TelephonyManager.NETWORK_TYPE_IWLAN:
|
||||
return C.NETWORK_TYPE_WIFI;
|
||||
case TelephonyManager.NETWORK_TYPE_GSM:
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ track 1:
|
|||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 59, hash A0217393
|
||||
data = length 59, hash 1AD38625
|
||||
sample 1:
|
||||
time = 2345000
|
||||
flags = 1
|
||||
data = length 95, hash 4904F2
|
||||
data = length 95, hash F331C282
|
||||
sample 2:
|
||||
time = 4567000
|
||||
flags = 1
|
||||
data = length 59, hash EFAB6D8A
|
||||
data = length 59, hash F8CD7C60
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ track 1:
|
|||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 59, hash A0217393
|
||||
data = length 59, hash 1AD38625
|
||||
sample 1:
|
||||
time = 2345000
|
||||
flags = 1
|
||||
data = length 95, hash 4904F2
|
||||
data = length 95, hash F331C282
|
||||
sample 2:
|
||||
time = 4567000
|
||||
flags = 1
|
||||
data = length 59, hash EFAB6D8A
|
||||
data = length 59, hash F8CD7C60
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ track 1:
|
|||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 59, hash A0217393
|
||||
data = length 59, hash 1AD38625
|
||||
sample 1:
|
||||
time = 2345000
|
||||
flags = 1
|
||||
data = length 95, hash 4904F2
|
||||
data = length 95, hash F331C282
|
||||
sample 2:
|
||||
time = 4567000
|
||||
flags = 1
|
||||
data = length 59, hash EFAB6D8A
|
||||
data = length 59, hash F8CD7C60
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ track 1:
|
|||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 59, hash A0217393
|
||||
data = length 59, hash 1AD38625
|
||||
sample 1:
|
||||
time = 2345000
|
||||
flags = 1
|
||||
data = length 95, hash 4904F2
|
||||
data = length 95, hash F331C282
|
||||
sample 2:
|
||||
time = 4567000
|
||||
flags = 1
|
||||
data = length 59, hash EFAB6D8A
|
||||
data = length 59, hash F8CD7C60
|
||||
tracksEnded = true
|
||||
|
|
|
|||
BIN
library/core/src/test/assets/mp4/sample_ac4.mp4
Normal file
BIN
library/core/src/test/assets/mp4/sample_ac4.mp4
Normal file
Binary file not shown.
107
library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump
Normal file
107
library/core/src/test/assets/mp4/sample_ac4.mp4.0.dump
Normal 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
|
||||
107
library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump
Normal file
107
library/core/src/test/assets/mp4/sample_ac4.mp4.1.dump
Normal 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
|
||||
107
library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump
Normal file
107
library/core/src/test/assets/mp4/sample_ac4.mp4.2.dump
Normal 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
|
||||
107
library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump
Normal file
107
library/core/src/test/assets/mp4/sample_ac4.mp4.3.dump
Normal 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
|
||||
BIN
library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4
Normal file
BIN
library/core/src/test/assets/mp4/sample_ac4_fragmented.mp4
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
BIN
library/core/src/test/assets/wav/sample_ima_adpcm.wav
Normal file
BIN
library/core/src/test/assets/wav/sample_ima_adpcm.wav
Normal file
Binary file not shown.
75
library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump
Normal file
75
library/core/src/test/assets/wav/sample_ima_adpcm.wav.0.dump
Normal 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
|
||||
59
library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump
Normal file
59
library/core/src/test/assets/wav/sample_ima_adpcm.wav.1.dump
Normal 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
|
||||
47
library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump
Normal file
47
library/core/src/test/assets/wav/sample_ima_adpcm.wav.2.dump
Normal 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
|
||||
35
library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump
Normal file
35
library/core/src/test/assets/wav/sample_ima_adpcm.wav.3.dump
Normal 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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
WEBVTT
|
||||
|
||||
STYLE
|
||||
::cue(\n#id ){text-decoration:underline;}
|
||||
::cue(#id ){text-decoration:underline;}
|
||||
|
||||
STYLE
|
||||
::cue(#id.class1.class2 ){ color: violet;}
|
||||
|
|
@ -20,7 +20,7 @@ STYLE
|
|||
|
||||
id
|
||||
00:00.000 --> 00:01.001
|
||||
This should be underlined and <lang.class1.class2> courier and violet.
|
||||
This should be underlined and <lang.class1.class2>courier and violet.
|
||||
|
||||
íd
|
||||
00:02.000 --> 00:02.001
|
||||
|
|
@ -31,10 +31,10 @@ _id
|
|||
This <lang.class.another>should be courier and bold.
|
||||
|
||||
00:04.000 --> 00:04.001
|
||||
This <v Strider Trancos> shouldn't be bold.</v>
|
||||
This <v.class.clazz Strider Trancos> should be bold.
|
||||
This <v Strider Trancos>shouldn't be bold.</v>
|
||||
This <v.class.clazz Strider Trancos>should be bold.
|
||||
|
||||
anId
|
||||
00:05.000 --> 00:05.001
|
||||
This is <v.class1.class3.class2 Pipo> specific </v>
|
||||
<v.class1.class3.class2 Robert> But this is more italic</v>
|
||||
This is <v.class1.class3.class2 Pipo>specific</v>
|
||||
<v.class1.class3.class2 Robert>But this is more italic</v>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -8,12 +8,12 @@ This is the first subtitle.
|
|||
NOTE Wrong position provided. It should be provided as
|
||||
a percentage value
|
||||
|
||||
00:02.345 --> 00:03.456 position:10 align:end size:35%
|
||||
00:02.345 --> 00:03.456 position:10 align:end
|
||||
This is the second subtitle.
|
||||
|
||||
NOTE Line as percentage and line alignment
|
||||
|
||||
00:04.000 --> 00:05.000 line:45%,end align:middle size:35%
|
||||
00:04.000 --> 00:05.000 line:45%,end align:middle
|
||||
This is the third subtitle.
|
||||
|
||||
NOTE Line as absolute negative number and without line alignment.
|
||||
|
|
@ -23,10 +23,10 @@ This is the fourth subtitle.
|
|||
|
||||
NOTE The position and positioning alignment should be inherited from align.
|
||||
|
||||
00:07.000 --> 00:08.000 align:right
|
||||
00:08.000 --> 00:09.000 align:right
|
||||
This is the fifth subtitle.
|
||||
|
||||
NOTE In newer drafts, align:middle has been replaced by align:center
|
||||
|
||||
00:10.000 --> 00:11.000 line:45%,end align:center size:35%
|
||||
00:10.000 --> 00:11.000 align:center
|
||||
This is the sixth subtitle.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<>());
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.flac;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
|
|
@ -66,9 +61,7 @@ public class FlacExtractorTest {
|
|||
|
||||
@Test
|
||||
public void testOneMetadataBlock() throws Exception {
|
||||
// Don't simulate IO errors as it is too slow when using the binary search seek map (see
|
||||
// [Internal: b/145994869]).
|
||||
assertBehaviorWithoutSimulatingIOErrors("flac/bear_one_metadata_block.flac");
|
||||
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_one_metadata_block.flac");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -85,61 +78,4 @@ public class FlacExtractorTest {
|
|||
public void testUncommonSampleRate() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(FlacExtractor::new, "flac/bear_uncommon_sample_rate.flac");
|
||||
}
|
||||
|
||||
private static void assertBehaviorWithoutSimulatingIOErrors(String file)
|
||||
throws IOException, InterruptedException {
|
||||
// Check behavior prior to initialization.
|
||||
Extractor extractor = new FlacExtractor();
|
||||
extractor.seek(0, 0);
|
||||
extractor.release();
|
||||
|
||||
// Assert output.
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
byte[] data = TestUtil.getByteArray(context, file);
|
||||
ExtractorAsserts.assertOutput(
|
||||
new FlacExtractor(),
|
||||
file,
|
||||
data,
|
||||
context,
|
||||
/* sniffFirst= */ true,
|
||||
/* simulateIOErrors= */ false,
|
||||
/* simulateUnknownLength= */ false,
|
||||
/* simulatePartialReads= */ false);
|
||||
ExtractorAsserts.assertOutput(
|
||||
new FlacExtractor(),
|
||||
file,
|
||||
data,
|
||||
context,
|
||||
/* sniffFirst= */ true,
|
||||
/* simulateIOErrors= */ false,
|
||||
/* simulateUnknownLength= */ false,
|
||||
/* simulatePartialReads= */ true);
|
||||
ExtractorAsserts.assertOutput(
|
||||
new FlacExtractor(),
|
||||
file,
|
||||
data,
|
||||
context,
|
||||
/* sniffFirst= */ true,
|
||||
/* simulateIOErrors= */ false,
|
||||
/* simulateUnknownLength= */ true,
|
||||
/* simulatePartialReads= */ false);
|
||||
ExtractorAsserts.assertOutput(
|
||||
new FlacExtractor(),
|
||||
file,
|
||||
data,
|
||||
context,
|
||||
/* sniffFirst= */ true,
|
||||
/* simulateIOErrors= */ false,
|
||||
/* simulateUnknownLength= */ true,
|
||||
/* simulatePartialReads= */ true);
|
||||
ExtractorAsserts.assertOutput(
|
||||
new FlacExtractor(),
|
||||
file,
|
||||
data,
|
||||
context,
|
||||
/* sniffFirst= */ false,
|
||||
/* simulateIOErrors= */ false,
|
||||
/* simulateUnknownLength= */ false,
|
||||
/* simulatePartialReads= */ false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ public final class FragmentedMp4ExtractorTest {
|
|||
ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSampleWithAc4Track() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
getExtractorFactory(Collections.emptyList()), "mp4/sample_ac4_fragmented.mp4");
|
||||
}
|
||||
|
||||
private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
|
||||
return () -> new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,4 +42,9 @@ public final class Mp4ExtractorTest {
|
|||
public void testMp4SampleWithMdatTooLong() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMp4SampleWithAc4Track() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
|
@ -172,6 +173,7 @@ public final class TsExtractorTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
|
||||
if (provideCustomEsReader && streamType == 3) {
|
||||
|
|
|
|||
|
|
@ -28,4 +28,9 @@ public final class WavExtractorTest {
|
|||
public void testSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSampleImaAdpcm() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec;
|
|||
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual;
|
||||
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
|
|
@ -29,7 +29,7 @@ import android.os.Looper;
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
|
@ -45,27 +45,32 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
private MediaCodec.BufferInfo bufferInfo;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
public void setUp() throws IOException {
|
||||
handlerThread = new HandlerThread("TestHandlerThread");
|
||||
handlerThread.start();
|
||||
looper = handlerThread.getLooper();
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
adapter = new AsynchronousMediaCodecAdapter(codec, looper);
|
||||
adapter.setCodecStartRunnable(() -> {});
|
||||
bufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
adapter.shutdown();
|
||||
handlerThread.quit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withoutInputBuffer_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withInputBuffer_returnsInputBuffer() {
|
||||
adapter.start();
|
||||
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
|
||||
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(0);
|
||||
|
|
@ -73,6 +78,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_whileFlushing_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0);
|
||||
adapter.flush();
|
||||
adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 1);
|
||||
|
|
@ -83,9 +89,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
@Test
|
||||
public void dequeueInputBufferIndex_afterFlushCompletes_returnsNextInputBuffer()
|
||||
throws InterruptedException {
|
||||
// Disable calling codec.start() after flush() completes to avoid receiving buffers from the
|
||||
// shadow codec impl
|
||||
adapter.setOnCodecStart(() -> {});
|
||||
adapter.start();
|
||||
Handler handler = new Handler(looper);
|
||||
handler.post(
|
||||
() -> adapter.getMediaCodecCallback().onInputBufferAvailable(codec, /* index=*/ 0));
|
||||
|
|
@ -100,28 +104,35 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
@Test
|
||||
public void dequeueInputBufferIndex_afterFlushCompletesWithError_throwsException()
|
||||
throws InterruptedException {
|
||||
adapter.setOnCodecStart(
|
||||
AtomicInteger calls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
throw new IllegalStateException("codec#start() exception");
|
||||
if (calls.incrementAndGet() == 2) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withoutOutputBuffer_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
|
||||
assertThat(adapter.dequeueOutputBufferIndex(bufferInfo))
|
||||
.isEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withOutputBuffer_returnsOutputBuffer() {
|
||||
adapter.start();
|
||||
MediaCodec.BufferInfo outBufferInfo = new MediaCodec.BufferInfo();
|
||||
outBufferInfo.presentationTimeUs = 10;
|
||||
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, outBufferInfo);
|
||||
|
|
@ -132,6 +143,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_whileFlushing_returnsTryAgainLater() {
|
||||
adapter.start();
|
||||
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 0, bufferInfo);
|
||||
adapter.flush();
|
||||
adapter.getMediaCodecCallback().onOutputBufferAvailable(codec, /* index=*/ 1, bufferInfo);
|
||||
|
|
@ -143,9 +155,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
@Test
|
||||
public void dequeueOutputBufferIndex_afterFlushCompletes_returnsNextOutputBuffer()
|
||||
throws InterruptedException {
|
||||
// Disable calling codec.start() after flush() completes to avoid receiving buffers from the
|
||||
// shadow codec impl
|
||||
adapter.setOnCodecStart(() -> {});
|
||||
adapter.start();
|
||||
Handler handler = new Handler(looper);
|
||||
MediaCodec.BufferInfo info0 = new MediaCodec.BufferInfo();
|
||||
handler.post(
|
||||
|
|
@ -164,31 +174,23 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
@Test
|
||||
public void dequeueOutputBufferIndex_afterFlushCompletesWithError_throwsException()
|
||||
throws InterruptedException {
|
||||
adapter.setOnCodecStart(
|
||||
AtomicInteger calls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
if (calls.incrementAndGet() == 2) {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withoutFormat_throwsException() {
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withMultipleFormats_returnsFormatsInCorrectOrder() {
|
||||
adapter.start();
|
||||
MediaFormat[] formats = new MediaFormat[10];
|
||||
MediaCodec.Callback mediaCodecCallback = adapter.getMediaCodecCallback();
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
|
|
@ -212,6 +214,7 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
|
||||
@Test
|
||||
public void getOutputFormat_afterFlush_returnsPreviousFormat() throws InterruptedException {
|
||||
adapter.start();
|
||||
MediaFormat format = new MediaFormat();
|
||||
adapter.getMediaCodecCallback().onOutputFormatChanged(codec, format);
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
|
|
@ -223,13 +226,13 @@ public class AsynchronousMediaCodecAdapterTest {
|
|||
|
||||
@Test
|
||||
public void shutdown_withPendingFlush_cancelsFlush() throws InterruptedException {
|
||||
AtomicBoolean onCodecStartCalled = new AtomicBoolean(false);
|
||||
Runnable onCodecStart = () -> onCodecStartCalled.set(true);
|
||||
adapter.setOnCodecStart(onCodecStart);
|
||||
AtomicInteger onCodecStartCalled = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> onCodecStartCalled.incrementAndGet());
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
adapter.shutdown();
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(onCodecStartCalled.get()).isFalse();
|
||||
assertThat(onCodecStartCalled.get()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec;
|
|||
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual;
|
||||
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
|
|
@ -47,16 +47,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
private MediaCodec.BufferInfo bufferInfo = null;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
public void setUp() throws IOException {
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
handlerThread = new TestHandlerThread("TestHandlerThread");
|
||||
adapter = new DedicatedThreadAsyncMediaCodecAdapter(codec, handlerThread);
|
||||
adapter.setCodecStartRunnable(() -> {});
|
||||
bufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
adapter.shutdown();
|
||||
|
||||
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
|
||||
}
|
||||
|
||||
|
|
@ -66,42 +68,15 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
adapter.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_calledTwice_throwsException() {
|
||||
adapter.start();
|
||||
try {
|
||||
adapter.start();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withoutStart_throwsException() {
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
|
||||
throws InterruptedException {
|
||||
adapter.setOnCodecStart(
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
throw new IllegalStateException("codec#start() exception");
|
||||
if (codecStartCalls.incrementAndGet() == 2) {
|
||||
throw new IllegalStateException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
|
@ -110,11 +85,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
waitUntilAllEventsAreExecuted(
|
||||
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
|
||||
.isTrue();
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -144,9 +116,6 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
@Test
|
||||
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer()
|
||||
throws InterruptedException {
|
||||
// Disable calling codec.start() after flush to avoid receiving buffers from the
|
||||
// shadow codec impl
|
||||
adapter.setOnCodecStart(() -> {});
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
|
|
@ -169,39 +138,18 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
adapter.start();
|
||||
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
|
||||
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withoutStart_throwsException() {
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withInternalException_throwsException()
|
||||
throws InterruptedException {
|
||||
adapter.setOnCodecStart(
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
if (codecStartCalls.incrementAndGet() == 2) {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
|
@ -210,11 +158,7 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
waitUntilAllEventsAreExecuted(
|
||||
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
|
||||
.isTrue();
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -275,42 +219,14 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
adapter.start();
|
||||
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
|
||||
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withoutStart_throwsException() {
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withoutFormatReceived_throwsException() {
|
||||
adapter.start();
|
||||
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -351,28 +267,10 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_withoutStarted_throwsException() {
|
||||
try {
|
||||
adapter.flush();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.flush();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException {
|
||||
AtomicInteger onCodecStartCount = new AtomicInteger(0);
|
||||
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
|
|
@ -384,23 +282,23 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
adapter.flush(); // Enqueues a second flush event
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
|
||||
|
||||
// Progress the looper until the milestoneCount is increased - first flush event
|
||||
// should have been a no-op
|
||||
// Progress the looper until the milestoneCount is increased.
|
||||
// adapter.start() will call codec.start(). First flush event should not call codec.start().
|
||||
ShadowLooper shadowLooper = shadowOf(looper);
|
||||
while (milestoneCount.get() < 1) {
|
||||
shadowLooper.runOneTask();
|
||||
}
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(0);
|
||||
assertThat(codecStartCalls.get()).isEqualTo(1);
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(1);
|
||||
assertThat(codecStartCalls.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException {
|
||||
AtomicInteger onCodecStartCount = new AtomicInteger(0);
|
||||
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
|
||||
adapter.setCodecStartRunnable(() -> onCodecStartCount.incrementAndGet());
|
||||
adapter.start();
|
||||
// Obtain looper when adapter is started
|
||||
Looper looper = handlerThread.getLooper();
|
||||
|
|
@ -408,8 +306,8 @@ public class DedicatedThreadAsyncMediaCodecAdapterTest {
|
|||
adapter.shutdown();
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue();
|
||||
// only shutdown flushes the MediaCodecAsync handler
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(0);
|
||||
// Only adapter.start() calls onCodecStart.
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static class TestHandlerThread extends HandlerThread {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package com.google.android.exoplayer2.mediacodec;
|
|||
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.areEqual;
|
||||
import static com.google.android.exoplayer2.mediacodec.MediaCodecTestUtils.waitUntilAllEventsAreExecuted;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
|
|
@ -44,20 +44,21 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
private MultiLockAsyncMediaCodecAdapter adapter;
|
||||
private MediaCodec codec;
|
||||
private MediaCodec.BufferInfo bufferInfo = null;
|
||||
private MediaCodecAsyncCallback mediaCodecAsyncCallbackSpy;
|
||||
private TestHandlerThread handlerThread;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
public void setUp() throws IOException {
|
||||
codec = MediaCodec.createByCodecName("h264");
|
||||
handlerThread = new TestHandlerThread("TestHandlerThread");
|
||||
adapter = new MultiLockAsyncMediaCodecAdapter(codec, handlerThread);
|
||||
adapter.setCodecStartRunnable(() -> {});
|
||||
bufferInfo = new MediaCodec.BufferInfo();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
adapter.shutdown();
|
||||
|
||||
assertThat(TestHandlerThread.INSTANCES_STARTED.get()).isEqualTo(0);
|
||||
}
|
||||
|
||||
|
|
@ -67,42 +68,15 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
adapter.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_calledTwice_throwsException() {
|
||||
adapter.start();
|
||||
try {
|
||||
adapter.start();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withoutStart_throwsException() {
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueInputBufferIndex_withAfterFlushFailed_throwsException()
|
||||
throws InterruptedException {
|
||||
adapter.setOnCodecStart(
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
throw new IllegalStateException("codec#start() exception");
|
||||
if (codecStartCalls.incrementAndGet() == 2) {
|
||||
throw new IllegalStateException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
|
@ -111,11 +85,7 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
waitUntilAllEventsAreExecuted(
|
||||
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
|
||||
.isTrue();
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -145,9 +115,6 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
@Test
|
||||
public void dequeueInputBufferIndex_withFlushCompletedAndInputBuffer_returnsInputBuffer()
|
||||
throws InterruptedException {
|
||||
// Disable calling codec.start() after flush to avoid receiving buffers from the
|
||||
// shadow codec impl
|
||||
adapter.setOnCodecStart(() -> {});
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
|
|
@ -170,39 +137,19 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
adapter.start();
|
||||
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
|
||||
|
||||
try {
|
||||
adapter.dequeueInputBufferIndex();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueInputBufferIndex());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withoutStart_throwsException() {
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBufferIndex_withInternalException_throwsException()
|
||||
throws InterruptedException {
|
||||
adapter.setOnCodecStart(
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(
|
||||
() -> {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
if (codecStartCalls.incrementAndGet() == 2) {
|
||||
throw new RuntimeException("codec#start() exception");
|
||||
}
|
||||
});
|
||||
adapter.start();
|
||||
adapter.flush();
|
||||
|
|
@ -211,11 +158,7 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
waitUntilAllEventsAreExecuted(
|
||||
handlerThread.getLooper(), /* time= */ 5, TimeUnit.SECONDS))
|
||||
.isTrue();
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -276,42 +219,14 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
adapter.start();
|
||||
adapter.onMediaCodecError(new IllegalStateException("error from codec"));
|
||||
|
||||
try {
|
||||
adapter.dequeueOutputBufferIndex(bufferInfo);
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withoutStart_throwsException() {
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.dequeueOutputBufferIndex(bufferInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOutputFormat_withoutFormatReceived_throwsException() {
|
||||
adapter.start();
|
||||
|
||||
try {
|
||||
adapter.getOutputFormat();
|
||||
fail();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
assertThrows(IllegalStateException.class, () -> adapter.getOutputFormat());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -352,28 +267,10 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
assertThat(adapter.getOutputFormat()).isEqualTo(format);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_withoutStarted_throwsException() {
|
||||
try {
|
||||
adapter.flush();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_afterShutdown_throwsException() {
|
||||
adapter.start();
|
||||
adapter.shutdown();
|
||||
try {
|
||||
adapter.flush();
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_multipleTimes_onlyLastFlushExecutes() throws InterruptedException {
|
||||
AtomicInteger onCodecStartCount = new AtomicInteger(0);
|
||||
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
|
||||
adapter.start();
|
||||
Looper looper = handlerThread.getLooper();
|
||||
Handler handler = new Handler(looper);
|
||||
|
|
@ -385,23 +282,23 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
adapter.flush(); // Enqueues a second flush event
|
||||
handler.post(() -> adapter.onInputBufferAvailable(codec, 3));
|
||||
|
||||
// Progress the looper until the milestoneCount is increased - first flush event
|
||||
// should have been a no-op
|
||||
// Progress the looper until the milestoneCount is increased:
|
||||
// adapter.start() called codec.start() but first flush event should have been a no-op
|
||||
ShadowLooper shadowLooper = shadowOf(looper);
|
||||
while (milestoneCount.get() < 1) {
|
||||
shadowLooper.runOneTask();
|
||||
}
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(0);
|
||||
assertThat(codecStartCalls.get()).isEqualTo(1);
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, /* time= */ 5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(adapter.dequeueInputBufferIndex()).isEqualTo(3);
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(1);
|
||||
assertThat(codecStartCalls.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_andImmediatelyShutdown_flushIsNoOp() throws InterruptedException {
|
||||
AtomicInteger onCodecStartCount = new AtomicInteger(0);
|
||||
adapter.setOnCodecStart(() -> onCodecStartCount.incrementAndGet());
|
||||
AtomicInteger codecStartCalls = new AtomicInteger(0);
|
||||
adapter.setCodecStartRunnable(() -> codecStartCalls.incrementAndGet());
|
||||
adapter.start();
|
||||
// Obtain looper when adapter is started.
|
||||
Looper looper = handlerThread.getLooper();
|
||||
|
|
@ -409,8 +306,8 @@ public class MultiLockAsyncMediaCodecAdapterTest {
|
|||
adapter.shutdown();
|
||||
|
||||
assertThat(waitUntilAllEventsAreExecuted(looper, 5, TimeUnit.SECONDS)).isTrue();
|
||||
// Only shutdown flushes the MediaCodecAsync handler.
|
||||
assertThat(onCodecStartCount.get()).isEqualTo(0);
|
||||
// Only adapter.start() called codec#start()
|
||||
assertThat(codecStartCalls.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static class TestHandlerThread extends HandlerThread {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue