Add supports for Seeking in AMR format using a constant bitrate seekmap.

- Extract ConstantBitrateSeeker from Mp3 package into a more general
ConstantBitrateSeekMap.
- Use this seekmap to implement seeking for AMR format.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=202638183
This commit is contained in:
hoangtc 2018-06-29 06:31:45 -07:00 committed by Oliver Woodman
parent efa714ab4f
commit 824c0b20a5
7 changed files with 913 additions and 72 deletions

View File

@ -4,6 +4,11 @@
* MediaSession extension:
* Allow apps to set custom errors.
* Audio:
* Support seeking for the AMR container format using constant bitrate seek
map.
* Add support for mu-law and A-law PCM with the ffmpeg extension
([#4360](https://github.com/google/ExoPlayer/issues/4360)).
* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when
creating a `CacheDataSource`.
* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend
@ -31,8 +36,6 @@
two additional convenience methods `Player.getTotalBufferedDuration` and
`Player.getContentBufferedDuration`
([#4023](https://github.com/google/ExoPlayer/issues/4023)).
* Add support for mu-law and A-law PCM with the ffmpeg extension
([#4360](https://github.com/google/ExoPlayer/issues/4360)).
* MediaSession extension:
* Allow apps to set custom metadata with a MediaMetadataProvider
([#3497](https://github.com/google/ExoPlayer/issues/3497)).

View File

@ -77,6 +77,9 @@ public final class C {
*/
public static final long NANOS_PER_SECOND = 1000000000L;
/** The number of bits per byte. */
public static final int BITS_PER_BYTE = 8;
/**
* The name of the ASCII charset.
*/

View File

@ -0,0 +1,123 @@
/*
* 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;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
/**
* A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of
* multiple independent frames of the same size. Seek points are calculated to be at frame
* boundaries.
*/
public class ConstantBitrateSeekMap implements SeekMap {
private final long inputLength;
private final long firstFrameBytePosition;
private final int frameSize;
private final long dataSize;
private final int bitrate;
private final long durationUs;
/**
* Constructs a new instance from a stream.
*
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFrameBytePosition The byte-position of the first frame in the stream.
* @param bitrate The bitrate (which is assumed to be constant in the stream).
* @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}
* if unknown.
*/
public ConstantBitrateSeekMap(
long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) {
this.inputLength = inputLength;
this.firstFrameBytePosition = firstFrameBytePosition;
this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize;
this.bitrate = bitrate;
if (inputLength == C.LENGTH_UNSET) {
dataSize = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
} else {
dataSize = inputLength - firstFrameBytePosition;
durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate);
}
}
@Override
public boolean isSeekable() {
return dataSize != C.LENGTH_UNSET;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
if (dataSize == C.LENGTH_UNSET) {
return new SeekPoints(new SeekPoint(0, firstFrameBytePosition));
}
long seekFramePosition = getFramePositionForTimeUs(timeUs);
long seekTimeUs = getTimeUsAtPosition(seekFramePosition);
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition);
if (seekTimeUs >= timeUs || seekFramePosition + frameSize >= inputLength) {
return new SeekPoints(seekPoint);
} else {
long secondSeekPosition = seekFramePosition + frameSize;
long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition);
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint);
}
}
@Override
public long getDurationUs() {
return durationUs;
}
/**
* Returns the stream time in microseconds for a given position.
*
* @param position The stream byte-position.
* @return The stream time in microseconds for the given position.
*/
public long getTimeUsAtPosition(long position) {
return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate);
}
// Internal methods
/**
* Returns the stream time in microseconds for a given stream position.
*
* @param position The stream byte-position.
* @param firstFrameBytePosition The position of the first frame in the stream.
* @param bitrate The bitrate (which is assumed to be constant in the stream).
* @return The stream time in microseconds for the given stream position.
*/
private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) {
return Math.max(0, position - firstFrameBytePosition)
* C.BITS_PER_BYTE
* C.MICROS_PER_SECOND
/ bitrate;
}
private long getFramePositionForTimeUs(long timeUs) {
long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE);
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / frameSize) * frameSize;
positionOffset =
Util.constrainValue(positionOffset, /* min= */ 0, /* max= */ dataSize - frameSize);
return firstFrameBytePosition + positionOffset;
}
}

View File

@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer2.extractor.amr;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
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;
@ -29,6 +32,8 @@ 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.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
/**
@ -49,6 +54,18 @@ public final class AmrExtractor implements Extractor {
}
};
/** Flags controlling the behavior of the extractor. */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING})
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;
/**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
* narrow band.
@ -100,23 +117,43 @@ public final class AmrExtractor implements Extractor {
/** 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 currentSampleTotalBytes;
private int currentSampleSize;
private int currentSampleBytesRemaining;
private boolean hasOutputSeekMap;
private long firstSamplePosition;
private int firstSampleSize;
private int numSamplesWithSameSize;
private long timeOffsetUs;
private ExtractorOutput extractorOutput;
private TrackOutput trackOutput;
private @Nullable SeekMap seekMap;
private boolean hasOutputFormat;
public AmrExtractor() {
this(/* flags= */ 0);
}
/** @param flags Flags that control the extractor's behavior. */
public AmrExtractor(@Flags int flags) {
this.flags = flags;
scratch = new byte[1];
firstSampleSize = C.LENGTH_UNSET;
}
// Extractor implementation.
@ -127,10 +164,10 @@ public final class AmrExtractor implements Extractor {
}
@Override
public void init(ExtractorOutput output) {
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
output.endTracks();
public void init(ExtractorOutput extractorOutput) {
this.extractorOutput = extractorOutput;
trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks();
}
@Override
@ -142,14 +179,21 @@ public final class AmrExtractor implements Extractor {
}
}
maybeOutputFormat();
return readSample(input);
int sampleReadResult = readSample(input);
maybeOutputSeekMap(input.getLength(), sampleReadResult);
return sampleReadResult;
}
@Override
public void seek(long position, long timeUs) {
currentSampleTimeUs = 0;
currentSampleTotalBytes = 0;
currentSampleSize = 0;
currentSampleBytesRemaining = 0;
if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) {
timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position);
} else {
timeOffsetUs = 0;
}
}
@Override
@ -228,11 +272,18 @@ public final class AmrExtractor implements Extractor {
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (currentSampleBytesRemaining == 0) {
try {
currentSampleTotalBytes = readNextSampleSize(extractorInput);
currentSampleSize = peekNextSampleSize(extractorInput);
} catch (EOFException e) {
return RESULT_END_OF_INPUT;
}
currentSampleBytesRemaining = currentSampleTotalBytes;
currentSampleBytesRemaining = currentSampleSize;
if (firstSampleSize == C.LENGTH_UNSET) {
firstSamplePosition = extractorInput.getPosition();
firstSampleSize = currentSampleSize;
}
if (firstSampleSize == currentSampleSize) {
numSamplesWithSameSize++;
}
}
int bytesAppended =
@ -247,16 +298,16 @@ public final class AmrExtractor implements Extractor {
}
trackOutput.sampleMetadata(
currentSampleTimeUs,
timeOffsetUs + currentSampleTimeUs,
C.BUFFER_FLAG_KEY_FRAME,
currentSampleTotalBytes,
currentSampleSize,
/* offset= */ 0,
/* encryptionData= */ null);
currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
return RESULT_CONTINUE;
}
private int readNextSampleSize(ExtractorInput extractorInput)
private int peekNextSampleSize(ExtractorInput extractorInput)
throws IOException, InterruptedException {
extractorInput.resetPeekPosition();
extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);
@ -296,4 +347,39 @@ public final class AmrExtractor implements Extractor {
// For narrow band, type 12-14 are for future use.
return !isWideBand && (frameType < 12 || frameType > 14);
}
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);
extractorOutput.seekMap(seekMap);
hasOutputSeekMap = true;
}
}
private SeekMap getConstantBitrateSeekMap(long inputLength) {
int bitrate = getBitrateFromFrameSize(firstSampleSize, SAMPLE_TIME_PER_FRAME_US);
return new ConstantBitrateSeekMap(inputLength, firstSamplePosition, bitrate, firstSampleSize);
}
/**
* 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);
}
}

View File

@ -16,78 +16,27 @@
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util;
/**
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
*/
/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker {
private static final int BITS_PER_BYTE = 8;
private final long firstFramePosition;
private final int frameSize;
private final long dataSize;
private final int bitrate;
private final long durationUs;
/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap
implements Mp3Extractor.Seeker {
/**
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFramePosition The position of the first frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
*/
public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
MpegAudioHeader mpegAudioHeader) {
this.firstFramePosition = firstFramePosition;
this.frameSize = mpegAudioHeader.frameSize;
this.bitrate = mpegAudioHeader.bitrate;
if (inputLength == C.LENGTH_UNSET) {
dataSize = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
} else {
dataSize = inputLength - firstFramePosition;
durationUs = getTimeUs(inputLength);
}
}
@Override
public boolean isSeekable() {
return dataSize != C.LENGTH_UNSET;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
if (dataSize == C.LENGTH_UNSET) {
return new SeekPoints(new SeekPoint(0, firstFramePosition));
}
long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / frameSize) * frameSize;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize);
long seekPosition = firstFramePosition + positionOffset;
long seekTimeUs = getTimeUs(seekPosition);
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
if (seekTimeUs >= timeUs || positionOffset == dataSize - frameSize) {
return new SeekPoints(seekPoint);
} else {
long secondSeekPosition = seekPosition + frameSize;
long secondSeekTimeUs = getTimeUs(secondSeekPosition);
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint);
}
public ConstantBitrateSeeker(
long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) {
super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize);
}
@Override
public long getTimeUs(long position) {
return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE)
/ bitrate;
return getTimeUsAtPosition(position);
}
@Override
public long getDurationUs() {
return durationUs;
}
}

View File

@ -0,0 +1,205 @@
/*
* 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;
import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link ConstantBitrateSeekMap}. */
@RunWith(RobolectricTestRunner.class)
public final class ConstantBitrateSeekMapTest {
private ConstantBitrateSeekMap constantBitrateSeekMap;
@Test
public void testIsSeekable_forKnownInputLength_returnSeekable() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 1000,
/* firstFrameBytePosition= */ 0,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
assertThat(constantBitrateSeekMap.isSeekable()).isTrue();
}
@Test
public void testIsSeekable_forUnknownInputLength_returnUnseekable() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ C.LENGTH_UNSET,
/* firstFrameBytePosition= */ 0,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
assertThat(constantBitrateSeekMap.isSeekable()).isFalse();
}
@Test
public void testGetSeekPoints_forUnseekableInput_returnSeekPoint0() {
int firstBytePosition = 100;
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ C.LENGTH_UNSET,
/* firstFrameBytePosition= */ firstBytePosition,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 123);
assertThat(seekPoints.first.timeUs).isEqualTo(0);
assertThat(seekPoints.first.position).isEqualTo(firstBytePosition);
assertThat(seekPoints.second).isEqualTo(seekPoints.first);
}
@Test
public void testGetDurationUs_forKnownInputLength_returnCorrectDuration() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
// Bitrate = 8000 (bits/s) = 1000 (bytes/s)
// FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us
// Input length = 2300 (bytes), first frame = 100, so duration = 2_200_000 us.
assertThat(constantBitrateSeekMap.getDurationUs()).isEqualTo(2_200_000);
}
@Test
public void testGetDurationUs_forUnnnownInputLength_returnUnknownDuration() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ C.LENGTH_UNSET,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
assertThat(constantBitrateSeekMap.getDurationUs()).isEqualTo(C.TIME_UNSET);
}
@Test
public void testGetSeekPoints_forSeekableInput_forSyncPosition0_return1SeekPoint() {
int firstBytePosition = 100;
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ firstBytePosition,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 0);
assertThat(seekPoints.first.timeUs).isEqualTo(0);
assertThat(seekPoints.first.position).isEqualTo(firstBytePosition);
assertThat(seekPoints.second).isEqualTo(seekPoints.first);
}
@Test
public void testGetSeekPoints_forSeekableInput_forSeekPointAtSyncPosition_return1SeekPoint() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 1_200_000);
// Bitrate = 8000 (bits/s) = 1000 (bytes/s)
// FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us
assertThat(seekPoints.first.timeUs).isEqualTo(1_200_000);
assertThat(seekPoints.first.position).isEqualTo(1300);
assertThat(seekPoints.second).isEqualTo(seekPoints.first);
}
@Test
public void testGetSeekPoints_forSeekableInput_forNonSyncSeekPosition_return2SeekPoints() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 345_678);
// Bitrate = 8000 (bits/s) = 1000 (bytes/s)
// FrameSize = 100 (bytes), so 1 frame = 1s = 100_000 us
assertThat(seekPoints.first.timeUs).isEqualTo(300_000);
assertThat(seekPoints.first.position).isEqualTo(400);
assertThat(seekPoints.second.timeUs).isEqualTo(400_000);
assertThat(seekPoints.second.position).isEqualTo(500);
}
@Test
public void testGetSeekPoints_forSeekableInput_forSeekPointWithinLastFrame_return1SeekPoint() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 2_123_456);
assertThat(seekPoints.first.timeUs).isEqualTo(2_100_000);
assertThat(seekPoints.first.position).isEqualTo(2_200);
assertThat(seekPoints.second).isEqualTo(seekPoints.first);
}
@Test
public void testGetSeekPoints_forSeekableInput_forSeekPointAtEndOfStream_return1SeekPoint() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
SeekMap.SeekPoints seekPoints = constantBitrateSeekMap.getSeekPoints(/* timeUs= */ 2_200_000);
assertThat(seekPoints.first.timeUs).isEqualTo(2_100_000);
assertThat(seekPoints.first.position).isEqualTo(2_200);
assertThat(seekPoints.second).isEqualTo(seekPoints.first);
}
@Test
public void testGetTimeUsAtPosition_forPosition0_return0() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(0);
assertThat(timeUs).isEqualTo(0);
}
@Test
public void testGetTimeUsAtPosition_forPositionWithinStream_returnCorrectTime() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(1234);
assertThat(timeUs).isEqualTo(1_134_000);
}
@Test
public void testGetTimeUsAtPosition_forPositionAtEndOfStream_returnStreamDuration() {
constantBitrateSeekMap =
new ConstantBitrateSeekMap(
/* inputLength= */ 2_300,
/* firstFrameBytePosition= */ 100,
/* bitrate= */ 8_000,
/* frameSize= */ 100);
long timeUs = constantBitrateSeekMap.getTimeUsAtPosition(2300);
assertThat(timeUs).isEqualTo(constantBitrateSeekMap.getDurationUs());
}
}

View File

@ -0,0 +1,472 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
import java.util.Random;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
/** Unit test for {@link AmrExtractor} narrow-band AMR file. */
@RunWith(RobolectricTestRunner.class)
public final class AmrExtractorSeekTest {
private static final Random random = new Random(1234L);
private static final String NARROW_BAND_AMR_FILE = "amr/sample_nb.amr";
private static final int NARROW_BAND_FILE_DURATION_US = 4_360_000;
private static final String WIDE_BAND_AMR_FILE = "amr/sample_wb.amr";
private static final int WIDE_BAND_FILE_DURATION_US = 3_380_000;
private FakeTrackOutput expectedTrackOutput;
private DefaultDataSource dataSource;
private PositionHolder positionHolder;
private long totalInputLength;
@Before
public void setUp() {
dataSource =
new DefaultDataSourceFactory(RuntimeEnvironment.application, "UserAgent")
.createDataSource();
positionHolder = new PositionHolder();
}
@Test
public void testAmrExtractorReads_returnSeekableSeekMap_forNarrowBandAmr()
throws IOException, InterruptedException {
String fileName = NARROW_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput(), fileName);
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(NARROW_BAND_FILE_DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
@Test
public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forNarrowBandAmr()
throws IOException, InterruptedException {
String fileName = NARROW_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = 980_000;
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesSeekToEoF_extractsLastFrame_forNarrowBandAmr()
throws IOException, InterruptedException {
String fileName = NARROW_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = seekMap.getDurationUs();
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forNarrowBandAmr()
throws IOException, InterruptedException {
String fileName = NARROW_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long firstSeekTimeUs = 980_000;
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput, fileName);
long targetSeekTimeUs = 0;
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forNarrowBandAmr()
throws IOException, InterruptedException {
String fileName = NARROW_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long firstSeekTimeUs = 980_000;
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput, fileName);
long targetSeekTimeUs = 1_200_000;
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forNarrowBandAmr()
throws IOException, InterruptedException {
String fileName = NARROW_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long numSeek = 100;
for (long i = 0; i < numSeek; i++) {
long targetSeekTimeUs = random.nextInt(NARROW_BAND_FILE_DURATION_US + 1);
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
}
@Test
public void testAmrExtractorReads_returnSeekableSeekMap_forWideBandAmr()
throws IOException, InterruptedException {
String fileName = WIDE_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput(), fileName);
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(WIDE_BAND_FILE_DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
@Test
public void testSeeking_handlesSeekingToPositionInFile_extractsCorrectFrame_forWideBandAmr()
throws IOException, InterruptedException {
String fileName = WIDE_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = 980_000;
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesSeekToEoF_extractsLastFrame_forWideBandAmr()
throws IOException, InterruptedException {
String fileName = WIDE_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long targetSeekTimeUs = seekMap.getDurationUs();
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesSeekingBackward_extractsCorrectFrames_forWideBandAmr()
throws IOException, InterruptedException {
String fileName = WIDE_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long firstSeekTimeUs = 980_000;
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput, fileName);
long targetSeekTimeUs = 0;
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesSeekingForward_extractsCorrectFrames_forWideBandAmr()
throws IOException, InterruptedException {
String fileName = WIDE_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long firstSeekTimeUs = 980_000;
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput, fileName);
long targetSeekTimeUs = 1_200_000;
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
@Test
public void testSeeking_handlesRandomSeeks_extractsCorrectFrames_forWideBandAmr()
throws IOException, InterruptedException {
String fileName = WIDE_BAND_AMR_FILE;
expectedTrackOutput =
extractAllSamplesFromFileToExpectedOutput(RuntimeEnvironment.application, fileName);
totalInputLength = readInputLength(fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
SeekMap seekMap = extractSeekMap(extractor, extractorOutput, fileName);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
long numSeek = 100;
for (long i = 0; i < numSeek; i++) {
long targetSeekTimeUs = random.nextInt(NARROW_BAND_FILE_DURATION_US + 1);
int extractedFrameIndex =
seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput, fileName);
assertThat(extractedFrameIndex).isNotEqualTo(-1);
assertFirstFrameAfterSeekContainTargetSeekTime(
trackOutput, targetSeekTimeUs, extractedFrameIndex);
}
}
// Internal methods
private static String assetPathForFile(String fileName) {
return "asset:///" + fileName;
}
private long readInputLength(String fileName) throws IOException {
DataSpec dataSpec =
new DataSpec(
Uri.parse(assetPathForFile(fileName)),
/* absoluteStreamPosition= */ 0,
/* length= */ C.LENGTH_UNSET,
/* key= */ null);
long totalInputLength = dataSource.open(dataSpec);
Util.closeQuietly(dataSource);
return totalInputLength;
}
/**
* Seeks to the given seek time and keeps reading from input until we can extract at least one
* frame from the seek position, or until end-of-input is reached.
*
* @return The index of the first extracted frame written to the given {@code trackOutput} after
* the seek is completed, or -1 if the seek is completed without any extracted frame.
*/
private int seekToTimeUs(
AmrExtractor amrExtractor,
SeekMap seekMap,
long seekTimeUs,
FakeTrackOutput trackOutput,
String fileName)
throws IOException, InterruptedException {
int numSampleBeforeSeek = trackOutput.getSampleCount();
SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
long initialSeekLoadPosition = seekPoints.first.position;
amrExtractor.seek(initialSeekLoadPosition, seekTimeUs);
positionHolder.position = C.POSITION_UNSET;
ExtractorInput extractorInput =
getExtractorInputFromPosition(initialSeekLoadPosition, fileName);
int extractorReadResult = Extractor.RESULT_CONTINUE;
while (true) {
try {
// Keep reading until we can read at least one frame after seek
while (extractorReadResult == Extractor.RESULT_CONTINUE
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
extractorReadResult = amrExtractor.read(extractorInput, positionHolder);
}
} finally {
Util.closeQuietly(dataSource);
}
if (extractorReadResult == Extractor.RESULT_SEEK) {
extractorInput = getExtractorInputFromPosition(positionHolder.position, fileName);
extractorReadResult = Extractor.RESULT_CONTINUE;
} else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {
return -1;
} else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
// First index after seek = num sample before seek.
return numSampleBeforeSeek;
}
}
}
private @Nullable SeekMap extractSeekMap(
AmrExtractor extractor, FakeExtractorOutput output, String fileName)
throws IOException, InterruptedException {
try {
ExtractorInput input = getExtractorInputFromPosition(/* position= */ 0, fileName);
extractor.init(output);
while (output.seekMap == null) {
extractor.read(input, positionHolder);
}
} finally {
Util.closeQuietly(dataSource);
}
return output.seekMap;
}
private void assertFirstFrameAfterSeekContainTargetSeekTime(
FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {
int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs);
// Assert that after seeking, the first sample frame written to output contains the sample
// at seek time.
trackOutput.assertSample(
firstFrameIndexAfterSeek,
expectedTrackOutput.getSampleData(expectedSampleIndex),
expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),
expectedTrackOutput.getSampleFlags(expectedSampleIndex),
expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));
}
private int findTargetFrameInExpectedOutput(long seekTimeUs) {
List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();
for (int i = 0; i < sampleTimes.size() - 1; i++) {
long currentSampleTime = sampleTimes.get(i);
long nextSampleTime = sampleTimes.get(i + 1);
if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {
return i;
}
}
return sampleTimes.size() - 1;
}
private ExtractorInput getExtractorInputFromPosition(long position, String fileName)
throws IOException {
DataSpec dataSpec =
new DataSpec(
Uri.parse(assetPathForFile(fileName)), position, totalInputLength, /* key= */ null);
dataSource.open(dataSpec);
return new DefaultExtractorInput(dataSource, position, totalInputLength);
}
private FakeTrackOutput extractAllSamplesFromFileToExpectedOutput(
Context context, String fileName) throws IOException, InterruptedException {
byte[] data = TestUtil.getByteArray(context, fileName);
AmrExtractor extractor = new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
FakeExtractorOutput expectedOutput = new FakeExtractorOutput();
extractor.init(expectedOutput);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {}
return expectedOutput.trackOutputs.get(0);
}
}