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