mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
efa714ab4f
commit
824c0b20a5
@ -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)).
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user