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