blob: 2ba8b0d599afb7f4fdd2abb1143346251e54d440 [file] [log] [blame]
/*
* 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.extractor.amr;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Extracts data from the AMR containers format (either AMR or AMR-WB). This follows RFC-4867,
* section 5.
*
* <p>This extractor only supports single-channel AMR container formats.
*/
public final class AmrExtractor implements Extractor {
/** Factory for {@link AmrExtractor} instances. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new AmrExtractor()};
/**
* Flags controlling the behavior of the extractor. Possible flag values are {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} and {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS})
public @interface Flags {}
/**
* Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
* otherwise not be possible.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* Like {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, except that seeking is also enabled in
* cases where the content length (and hence the duration of the media) is unknown. Application
* code should ensure that requested seek positions are valid when using this flag, or be ready to
* handle playback failures reported through {@link Player.Listener#onPlayerError} with {@link
* PlaybackException#errorCode} set to {@link
* PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*
* <p>If this flag is set, then the behavior enabled by {@link
* #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} is implicitly enabled as well.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS = 1 << 1;
/**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
* narrow band.
*/
private static final int[] frameSizeBytesByTypeNb = {
13,
14,
16,
18,
20,
21,
27,
32,
6, // AMR SID
7, // GSM-EFR SID
6, // TDMA-EFR SID
6, // PDC-EFR SID
1, // Future use
1, // Future use
1, // Future use
1 // No data
};
/**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR wide
* band.
*/
private static final int[] frameSizeBytesByTypeWb = {
18,
24,
33,
37,
41,
47,
51,
59,
61,
6, // AMR-WB SID
1, // Future use
1, // Future use
1, // Future use
1, // Future use
1, // speech lost
1 // No data
};
private static final byte[] amrSignatureNb = Util.getUtf8Bytes("#!AMR\n");
private static final byte[] amrSignatureWb = Util.getUtf8Bytes("#!AMR-WB\n");
/** Theoretical maximum frame size for a AMR frame. */
private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8];
/**
* The required number of samples in the stream with same sample size to classify the stream as a
* constant-bitrate-stream.
*/
private static final int NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD = 20;
private static final int SAMPLE_RATE_WB = 16_000;
private static final int SAMPLE_RATE_NB = 8_000;
private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;
private final byte[] scratch;
private final @Flags int flags;
private boolean isWideBand;
private long currentSampleTimeUs;
private int currentSampleSize;
private int currentSampleBytesRemaining;
private boolean hasOutputSeekMap;
private long firstSamplePosition;
private int firstSampleSize;
private int numSamplesWithSameSize;
private long timeOffsetUs;
private @MonotonicNonNull ExtractorOutput extractorOutput;
private @MonotonicNonNull TrackOutput trackOutput;
private @MonotonicNonNull SeekMap seekMap;
private boolean hasOutputFormat;
public AmrExtractor() {
this(/* flags= */ 0);
}
/** @param flags Flags that control the extractor's behavior. */
public AmrExtractor(@Flags int flags) {
if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0) {
flags |= FLAG_ENABLE_CONSTANT_BITRATE_SEEKING;
}
this.flags = flags;
scratch = new byte[1];
firstSampleSize = C.LENGTH_UNSET;
}
// Extractor implementation.
@Override
public boolean sniff(ExtractorInput input) throws IOException {
return readAmrHeader(input);
}
@Override
public void init(ExtractorOutput output) {
this.extractorOutput = output;
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
output.endTracks();
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
assertInitialized();
if (input.getPosition() == 0) {
if (!readAmrHeader(input)) {
throw ParserException.createForMalformedContainer(
"Could not find AMR header.", /* cause= */ null);
}
}
maybeOutputFormat();
int sampleReadResult = readSample(input);
maybeOutputSeekMap(input.getLength(), sampleReadResult);
return sampleReadResult;
}
@Override
public void seek(long position, long timeUs) {
currentSampleTimeUs = 0;
currentSampleSize = 0;
currentSampleBytesRemaining = 0;
if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) {
timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position);
} else {
timeOffsetUs = 0;
}
}
@Override
public void release() {
// Do nothing
}
/* package */ static int frameSizeBytesByTypeNb(int frameType) {
return frameSizeBytesByTypeNb[frameType];
}
/* package */ static int frameSizeBytesByTypeWb(int frameType) {
return frameSizeBytesByTypeWb[frameType];
}
/* package */ static byte[] amrSignatureNb() {
return Arrays.copyOf(amrSignatureNb, amrSignatureNb.length);
}
/* package */ static byte[] amrSignatureWb() {
return Arrays.copyOf(amrSignatureWb, amrSignatureWb.length);
}
// Internal methods.
/**
* Peeks the AMR header from the beginning of the input, and consumes it if it exists.
*
* @param input The {@link ExtractorInput} from which data should be peeked/read.
* @return Whether the AMR header has been read.
*/
private boolean readAmrHeader(ExtractorInput input) throws IOException {
if (peekAmrSignature(input, amrSignatureNb)) {
isWideBand = false;
input.skipFully(amrSignatureNb.length);
return true;
} else if (peekAmrSignature(input, amrSignatureWb)) {
isWideBand = true;
input.skipFully(amrSignatureWb.length);
return true;
}
return false;
}
/** Peeks from the beginning of the input to see if the given AMR signature exists. */
private static boolean peekAmrSignature(ExtractorInput input, byte[] amrSignature)
throws IOException {
input.resetPeekPosition();
byte[] header = new byte[amrSignature.length];
input.peekFully(header, 0, amrSignature.length);
return Arrays.equals(header, amrSignature);
}
@RequiresNonNull("trackOutput")
private void maybeOutputFormat() {
if (!hasOutputFormat) {
hasOutputFormat = true;
String mimeType = isWideBand ? MimeTypes.AUDIO_AMR_WB : MimeTypes.AUDIO_AMR_NB;
int sampleRate = isWideBand ? SAMPLE_RATE_WB : SAMPLE_RATE_NB;
trackOutput.format(
new Format.Builder()
.setSampleMimeType(mimeType)
.setMaxInputSize(MAX_FRAME_SIZE_BYTES)
.setChannelCount(1)
.setSampleRate(sampleRate)
.build());
}
}
@RequiresNonNull("trackOutput")
private int readSample(ExtractorInput extractorInput) throws IOException {
if (currentSampleBytesRemaining == 0) {
try {
currentSampleSize = peekNextSampleSize(extractorInput);
} catch (EOFException e) {
return RESULT_END_OF_INPUT;
}
currentSampleBytesRemaining = currentSampleSize;
if (firstSampleSize == C.LENGTH_UNSET) {
firstSamplePosition = extractorInput.getPosition();
firstSampleSize = currentSampleSize;
}
if (firstSampleSize == currentSampleSize) {
numSamplesWithSameSize++;
}
}
int bytesAppended =
trackOutput.sampleData(
extractorInput, currentSampleBytesRemaining, /* allowEndOfInput= */ true);
if (bytesAppended == C.RESULT_END_OF_INPUT) {
return RESULT_END_OF_INPUT;
}
currentSampleBytesRemaining -= bytesAppended;
if (currentSampleBytesRemaining > 0) {
return RESULT_CONTINUE;
}
trackOutput.sampleMetadata(
timeOffsetUs + currentSampleTimeUs,
C.BUFFER_FLAG_KEY_FRAME,
currentSampleSize,
/* offset= */ 0,
/* encryptionData= */ null);
currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
return RESULT_CONTINUE;
}
private int peekNextSampleSize(ExtractorInput extractorInput) throws IOException {
extractorInput.resetPeekPosition();
extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);
byte frameHeader = scratch[0];
if ((frameHeader & 0x83) > 0) {
// The padding bits are at bit-1 positions in the following pattern: 1000 0011
// Padding bits must be 0.
throw ParserException.createForMalformedContainer(
"Invalid padding bits for frame header " + frameHeader, /* cause= */ null);
}
int frameType = (frameHeader >> 3) & 0x0f;
return getFrameSizeInBytes(frameType);
}
private int getFrameSizeInBytes(int frameType) throws ParserException {
if (!isValidFrameType(frameType)) {
throw ParserException.createForMalformedContainer(
"Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType,
/* cause= */ null);
}
return isWideBand ? frameSizeBytesByTypeWb[frameType] : frameSizeBytesByTypeNb[frameType];
}
private boolean isValidFrameType(int frameType) {
return frameType >= 0
&& frameType <= 15
&& (isWideBandValidFrameType(frameType) || isNarrowBandValidFrameType(frameType));
}
private boolean isWideBandValidFrameType(int frameType) {
// For wide band, type 10-13 are for future use.
return isWideBand && (frameType < 10 || frameType > 13);
}
private boolean isNarrowBandValidFrameType(int frameType) {
// For narrow band, type 12-14 are for future use.
return !isWideBand && (frameType < 12 || frameType > 14);
}
@RequiresNonNull("extractorOutput")
private void maybeOutputSeekMap(long inputLength, int sampleReadResult) {
if (hasOutputSeekMap) {
return;
}
if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) == 0
|| inputLength == C.LENGTH_UNSET
|| (firstSampleSize != C.LENGTH_UNSET && firstSampleSize != currentSampleSize)) {
seekMap = new SeekMap.Unseekable(C.TIME_UNSET);
extractorOutput.seekMap(seekMap);
hasOutputSeekMap = true;
} else if (numSamplesWithSameSize >= NUM_SAME_SIZE_CONSTANT_BIT_RATE_THRESHOLD
|| sampleReadResult == RESULT_END_OF_INPUT) {
seekMap =
getConstantBitrateSeekMap(
inputLength, (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0);
extractorOutput.seekMap(seekMap);
hasOutputSeekMap = true;
}
}
private SeekMap getConstantBitrateSeekMap(long inputLength, boolean allowSeeksIfLengthUnknown) {
int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US);
return new ConstantBitrateSeekMap(
inputLength, firstSamplePosition, bitrate, firstSampleSize, allowSeeksIfLengthUnknown);
}
@EnsuresNonNull({"extractorOutput", "trackOutput"})
private void assertInitialized() {
Assertions.checkStateNotNull(trackOutput);
Util.castNonNull(extractorOutput);
}
/**
* Returns the stream bitrate, given a frame size and the duration of that frame in microseconds.
*
* @param frameSize The size of each frame in the stream.
* @param durationUsPerFrame The duration of the given frame in microseconds.
* @return The stream bitrate.
*/
private static int getBitrateFromFrameSize(int frameSize, long durationUsPerFrame) {
return (int) ((frameSize * C.BITS_PER_BYTE * C.MICROS_PER_SECOND) / durationUsPerFrame);
}
}