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:
|
* MediaSession extension:
|
||||||
* Allow apps to set custom errors.
|
* 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
|
* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when
|
||||||
creating a `CacheDataSource`.
|
creating a `CacheDataSource`.
|
||||||
* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend
|
* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend
|
||||||
@ -31,8 +36,6 @@
|
|||||||
two additional convenience methods `Player.getTotalBufferedDuration` and
|
two additional convenience methods `Player.getTotalBufferedDuration` and
|
||||||
`Player.getContentBufferedDuration`
|
`Player.getContentBufferedDuration`
|
||||||
([#4023](https://github.com/google/ExoPlayer/issues/4023)).
|
([#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:
|
* MediaSession extension:
|
||||||
* Allow apps to set custom metadata with a MediaMetadataProvider
|
* Allow apps to set custom metadata with a MediaMetadataProvider
|
||||||
([#3497](https://github.com/google/ExoPlayer/issues/3497)).
|
([#3497](https://github.com/google/ExoPlayer/issues/3497)).
|
||||||
|
@ -77,6 +77,9 @@ public final class C {
|
|||||||
*/
|
*/
|
||||||
public static final long NANOS_PER_SECOND = 1000000000L;
|
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.
|
* 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;
|
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.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.extractor.ConstantBitrateSeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
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 com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.Arrays;
|
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
|
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR
|
||||||
* narrow band.
|
* narrow band.
|
||||||
@ -100,23 +117,43 @@ public final class AmrExtractor implements Extractor {
|
|||||||
|
|
||||||
/** Theoretical maximum frame size for a AMR frame. */
|
/** Theoretical maximum frame size for a AMR frame. */
|
||||||
private static final int MAX_FRAME_SIZE_BYTES = frameSizeBytesByTypeWb[8];
|
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_WB = 16_000;
|
||||||
private static final int SAMPLE_RATE_NB = 8_000;
|
private static final int SAMPLE_RATE_NB = 8_000;
|
||||||
private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;
|
private static final int SAMPLE_TIME_PER_FRAME_US = 20_000;
|
||||||
|
|
||||||
private final byte[] scratch;
|
private final byte[] scratch;
|
||||||
|
private final @Flags int flags;
|
||||||
|
|
||||||
private boolean isWideBand;
|
private boolean isWideBand;
|
||||||
private long currentSampleTimeUs;
|
private long currentSampleTimeUs;
|
||||||
private int currentSampleTotalBytes;
|
private int currentSampleSize;
|
||||||
private int currentSampleBytesRemaining;
|
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 TrackOutput trackOutput;
|
||||||
|
private @Nullable SeekMap seekMap;
|
||||||
private boolean hasOutputFormat;
|
private boolean hasOutputFormat;
|
||||||
|
|
||||||
public AmrExtractor() {
|
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];
|
scratch = new byte[1];
|
||||||
|
firstSampleSize = C.LENGTH_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extractor implementation.
|
// Extractor implementation.
|
||||||
@ -127,10 +164,10 @@ public final class AmrExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput extractorOutput) {
|
||||||
output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
|
this.extractorOutput = extractorOutput;
|
||||||
trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
|
trackOutput = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_AUDIO);
|
||||||
output.endTracks();
|
extractorOutput.endTracks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -142,14 +179,21 @@ public final class AmrExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
maybeOutputFormat();
|
maybeOutputFormat();
|
||||||
return readSample(input);
|
int sampleReadResult = readSample(input);
|
||||||
|
maybeOutputSeekMap(input.getLength(), sampleReadResult);
|
||||||
|
return sampleReadResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek(long position, long timeUs) {
|
public void seek(long position, long timeUs) {
|
||||||
currentSampleTimeUs = 0;
|
currentSampleTimeUs = 0;
|
||||||
currentSampleTotalBytes = 0;
|
currentSampleSize = 0;
|
||||||
currentSampleBytesRemaining = 0;
|
currentSampleBytesRemaining = 0;
|
||||||
|
if (position != 0 && seekMap instanceof ConstantBitrateSeekMap) {
|
||||||
|
timeOffsetUs = ((ConstantBitrateSeekMap) seekMap).getTimeUsAtPosition(position);
|
||||||
|
} else {
|
||||||
|
timeOffsetUs = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -228,11 +272,18 @@ public final class AmrExtractor implements Extractor {
|
|||||||
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
|
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
|
||||||
if (currentSampleBytesRemaining == 0) {
|
if (currentSampleBytesRemaining == 0) {
|
||||||
try {
|
try {
|
||||||
currentSampleTotalBytes = readNextSampleSize(extractorInput);
|
currentSampleSize = peekNextSampleSize(extractorInput);
|
||||||
} catch (EOFException e) {
|
} catch (EOFException e) {
|
||||||
return RESULT_END_OF_INPUT;
|
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 =
|
int bytesAppended =
|
||||||
@ -247,16 +298,16 @@ public final class AmrExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackOutput.sampleMetadata(
|
trackOutput.sampleMetadata(
|
||||||
currentSampleTimeUs,
|
timeOffsetUs + currentSampleTimeUs,
|
||||||
C.BUFFER_FLAG_KEY_FRAME,
|
C.BUFFER_FLAG_KEY_FRAME,
|
||||||
currentSampleTotalBytes,
|
currentSampleSize,
|
||||||
/* offset= */ 0,
|
/* offset= */ 0,
|
||||||
/* encryptionData= */ null);
|
/* encryptionData= */ null);
|
||||||
currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
|
currentSampleTimeUs += SAMPLE_TIME_PER_FRAME_US;
|
||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readNextSampleSize(ExtractorInput extractorInput)
|
private int peekNextSampleSize(ExtractorInput extractorInput)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
extractorInput.resetPeekPosition();
|
extractorInput.resetPeekPosition();
|
||||||
extractorInput.peekFully(scratch, /* offset= */ 0, /* length= */ 1);
|
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.
|
// For narrow band, type 12-14 are for future use.
|
||||||
return !isWideBand && (frameType < 12 || frameType > 14);
|
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;
|
package com.google.android.exoplayer2.extractor.mp3;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
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.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.
|
* MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
|
||||||
*/
|
*/
|
||||||
/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker {
|
/* package */ final class ConstantBitrateSeeker extends ConstantBitrateSeekMap
|
||||||
|
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
|
* @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 firstFramePosition The position of the first frame in the stream.
|
||||||
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
|
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
|
||||||
*/
|
*/
|
||||||
public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
|
public ConstantBitrateSeeker(
|
||||||
MpegAudioHeader mpegAudioHeader) {
|
long inputLength, long firstFramePosition, MpegAudioHeader mpegAudioHeader) {
|
||||||
this.firstFramePosition = firstFramePosition;
|
super(inputLength, firstFramePosition, mpegAudioHeader.bitrate, mpegAudioHeader.frameSize);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getTimeUs(long position) {
|
public long getTimeUs(long position) {
|
||||||
return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE)
|
return getTimeUsAtPosition(position);
|
||||||
/ bitrate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@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