Enhance SeekMaps to return SeekPoints

Issue: #2882

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177814974
This commit is contained in:
olly 2017-12-04 08:12:35 -08:00 committed by Oliver Woodman
parent bb0fae3ee8
commit fbfa43f5a3
70 changed files with 439 additions and 173 deletions

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor {
}
metadataParsed = true;
extractorOutput.seekMap(new SeekMap() {
final boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
final long durationUs = streamInfo.durationUs();
@Override
public boolean isSeekable() {
return isSeekable;
}
@Override
public long getPosition(long timeUs) {
return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0;
}
@Override
public long getDurationUs() {
return durationUs;
}
});
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
extractorOutput.seekMap(
isSeekable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
null,
@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor {
}
}
private static final class FlacSeekMap implements SeekMap {
private final long durationUs;
private final FlacDecoderJni decoderJni;
public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
this.durationUs = durationUs;
this.decoderJni = decoderJni;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
// TODO: Access the seek table via JNI to return two seek points when appropriate.
return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
}
@Override
public long getDurationUs() {
return durationUs;
}
}
}

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = 1136000
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2
track 8:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1072000
getPosition(0) = 5576
getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2
track 1:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1072000
getPosition(0) = 5576
getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2
track 1:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1072000
getPosition(0) = 5576
getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2
track 1:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1072000
getPosition(0) = 5576
getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2
track 1:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = 1000
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 1:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = 1000
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 1:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2784000
getPosition(0) = 201
getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2784000
getPosition(0) = 201
getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2784000
getPosition(0) = 201
getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2784000
getPosition(0) = 201
getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1024000
getPosition(0) = 48
getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1024000
getPosition(0) = 48
getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1024000
getPosition(0) = 48
getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1024000
getPosition(0) = 48
getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 1828
getPosition(0) = [[timeUs=0, position=1828]]
numberOfTracks = 2
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 1828
getPosition(0) = [[timeUs=0, position=1828]]
numberOfTracks = 3
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2747500
getPosition(0) = 125
getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2747500
getPosition(0) = 125
getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2747500
getPosition(0) = 125
getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2747500
getPosition(0) = 125
getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8457
getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8457
getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8457
getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8457
getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8457
getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8407
getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8407
getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8407
getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8407
getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 3995
getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 3995
getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 3995
getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 3995
getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2
track 0:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2
track 192:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2
track 256:
format:

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1000000
getPosition(0) = 78
getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1
track 0:
format:
@ -27,15 +27,15 @@ track 0:
initializationData:
sample count = 3
sample 0:
time = 884
time = 0
flags = 1
data = length 32768, hash 9A8CEEBA
sample 1:
time = 372403
time = 371519
flags = 1
data = length 32768, hash C1717317
sample 2:
time = 743922
time = 743038
flags = 1
data = length 22664, hash 819F5F62
tracksEnded = true

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1000000
getPosition(0) = 78
getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1
track 0:
format:
@ -27,11 +27,11 @@ track 0:
initializationData:
sample count = 2
sample 0:
time = 334195
time = 333310
flags = 1
data = length 32768, hash 42D6E860
sample 1:
time = 705714
time = 704829
flags = 1
data = length 26034, hash 62692C38
tracksEnded = true

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1000000
getPosition(0) = 78
getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1
track 0:
format:
@ -27,7 +27,7 @@ track 0:
initializationData:
sample count = 1
sample 0:
time = 667528
time = 666643
flags = 1
data = length 29402, hash 4241604E
tracksEnded = true

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 1000000
getPosition(0) = 78
getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1
track 0:
format:
@ -27,7 +27,7 @@ track 0:
initializationData:
sample count = 1
sample 0:
time = 1000861
time = 999977
flags = 1
data = length 2, hash 116
tracksEnded = true

View file

@ -91,8 +91,15 @@ public final class ChunkIndex implements SeekMap {
}
@Override
public long getPosition(long timeUs) {
return offsets[getChunkIndex(timeUs)];
public SeekPoints getSeekPoints(long timeUs) {
int chunkIndex = getChunkIndex(timeUs);
SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]);
if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) {
return new SeekPoints(seekPoint);
} else {
SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]);
return new SeekPoints(seekPoint, nextSeekPoint);
}
}
}

View file

@ -16,36 +16,36 @@
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
/**
* Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
*/
public interface SeekMap {
/**
* A {@link SeekMap} that does not support seeking.
*/
/** A {@link SeekMap} that does not support seeking. */
final class Unseekable implements SeekMap {
private final long durationUs;
private final long startPosition;
private final SeekPoints startSeekPoints;
/**
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
* the duration is unknown.
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
* duration is unknown.
*/
public Unseekable(long durationUs) {
this(durationUs, 0);
}
/**
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
* the duration is unknown.
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
* duration is unknown.
* @param startPosition The position (byte offset) of the start of the media.
*/
public Unseekable(long durationUs, long startPosition) {
this.durationUs = durationUs;
this.startPosition = startPosition;
startSeekPoints =
new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition));
}
@Override
@ -59,17 +59,58 @@ public interface SeekMap {
}
@Override
public long getPosition(long timeUs) {
return startPosition;
public SeekPoints getSeekPoints(long timeUs) {
return startSeekPoints;
}
}
/** Contains one or two {@link SeekPoint}s. */
final class SeekPoints {
/** The first seek point. */
public final SeekPoint first;
/** The second seek point, or {@link #first} if there's only one seek point. */
public final SeekPoint second;
/** @param point The single seek point. */
public SeekPoints(SeekPoint point) {
this(point, point);
}
/**
* @param first The first seek point.
* @param second The second seek point.
*/
public SeekPoints(SeekPoint first, SeekPoint second) {
this.first = Assertions.checkNotNull(first);
this.second = Assertions.checkNotNull(second);
}
@Override
public String toString() {
return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SeekPoints other = (SeekPoints) obj;
return first.equals(other.first) && second.equals(other.second);
}
@Override
public int hashCode() {
return (31 * first.hashCode()) + second.hashCode();
}
}
/**
* Returns whether seeking is supported.
* <p>
* If seeking is not supported then the only valid seek position is the start of the file, and so
* {@link #getPosition(long)} will return 0 for all input values.
*
* @return Whether seeking is supported.
*/
@ -78,20 +119,22 @@ public interface SeekMap {
/**
* Returns the duration of the stream in microseconds.
*
* @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
* duration is unknown.
* @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is
* unknown.
*/
long getDurationUs();
/**
* Maps a seek position in microseconds to a corresponding position (byte offset) in the stream
* from which data can be provided to the extractor.
* Obtains seek points for the specified seek time in microseconds. The returned {@link
* SeekPoints} will contain one or two distinct seek points.
*
* @param timeUs A seek position in microseconds.
* @return The corresponding position (byte offset) in the stream from which data can be provided
* to the extractor. If {@link #isSeekable()} returns false then the returned value will be
* independent of {@code timeUs}, and will indicate the start of the media in the stream.
* <p>Two seek points [A, B] are returned in the case that seeking can only be performed to
* discrete points in time, there does not exist a seek point at exactly the requested time, and
* there exist seek points on both sides of it. In this case A and B are the closest seek points
* before and after the requested time. A single seek point is returned in all other cases.
*
* @param timeUs A seek time in microseconds.
* @return The corresponding seek points.
*/
long getPosition(long timeUs);
SeekPoints getSeekPoints(long timeUs);
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (C) 2017 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;
/** Defines a seek point in a media stream. */
public final class SeekPoint {
/** A {@link SeekPoint} whose time and byte offset are both set to 0. */
public static final SeekPoint START = new SeekPoint(0, 0);
/** The time of the seek point, in microseconds. */
public final long timeUs;
/** The byte offset of the seek point. */
public final long position;
/**
* @param timeUs The time of the seek point, in microseconds.
* @param position The byte offset of the seek point.
*/
public SeekPoint(long timeUs, long position) {
this.timeUs = timeUs;
this.position = position;
}
@Override
public String toString() {
return "[timeUs=" + timeUs + ", position=" + position + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SeekPoint other = (SeekPoint) obj;
return timeUs == other.timeUs && position == other.position;
}
@Override
public int hashCode() {
int result = (int) timeUs;
result = 31 * result + (int) position;
return result;
}
}

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util;
/**
@ -57,16 +58,25 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
public long getPosition(long timeUs) {
public SeekPoints getSeekPoints(long timeUs) {
if (dataSize == C.LENGTH_UNSET) {
return firstFramePosition;
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);
// Add data start position.
return firstFramePosition + positionOffset;
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

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@ -106,8 +107,15 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
public long getPosition(long timeUs) {
return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)];
public SeekPoints getSeekPoints(long timeUs) {
int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true);
SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]);
if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) {
return new SeekPoints(seekPoint);
} else {
SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]);
return new SeekPoints(seekPoint, nextSeekPoint);
}
}
@Override

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@ -107,10 +108,11 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
public long getPosition(long timeUs) {
public SeekPoints getSeekPoints(long timeUs) {
if (!isSeekable()) {
return dataStartPosition + xingFrameSize;
return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize));
}
timeUs = Util.constrainValue(timeUs, 0, durationUs);
double percent = (timeUs * 100d) / durationUs;
double scaledPosition;
if (percent <= 0) {
@ -129,7 +131,7 @@ import com.google.android.exoplayer2.util.Util;
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
// Ensure returned positions skip the frame containing the XING header.
positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
return dataStartPosition + positionOffset;
return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset));
}
@Override

View file

@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.metadata.Metadata;
@ -108,6 +109,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Extractor outputs.
private ExtractorOutput extractorOutput;
private Mp4Track[] tracks;
private int firstVideoTrackIndex;
private long durationUs;
private boolean isQuickTime;
@ -196,21 +198,56 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
@Override
public long getPosition(long timeUs) {
long earliestSamplePosition = Long.MAX_VALUE;
for (Mp4Track track : tracks) {
TrackSampleTable sampleTable = track.sampleTable;
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
public SeekPoints getSeekPoints(long timeUs) {
if (tracks.length == 0) {
return new SeekPoints(SeekPoint.START);
}
long firstTimeUs;
long firstOffset;
long secondTimeUs = C.TIME_UNSET;
long secondOffset = C.POSITION_UNSET;
// If we have a video track, use it to establish one or two seek points.
if (firstVideoTrackIndex != C.INDEX_UNSET) {
TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable;
int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs);
if (sampleIndex == C.INDEX_UNSET) {
// Handle the case where the requested time is before the first synchronization sample.
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
return new SeekPoints(SeekPoint.START);
}
long offset = sampleTable.offsets[sampleIndex];
if (offset < earliestSamplePosition) {
earliestSamplePosition = offset;
long sampleTimeUs = sampleTable.timestampsUs[sampleIndex];
firstTimeUs = sampleTimeUs;
firstOffset = sampleTable.offsets[sampleIndex];
if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) {
int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) {
secondTimeUs = sampleTable.timestampsUs[secondSampleIndex];
secondOffset = sampleTable.offsets[secondSampleIndex];
}
}
} else {
firstTimeUs = timeUs;
firstOffset = Long.MAX_VALUE;
}
// Take into account other tracks.
for (int i = 0; i < tracks.length; i++) {
if (i != firstVideoTrackIndex) {
TrackSampleTable sampleTable = tracks[i].sampleTable;
firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset);
if (secondTimeUs != C.TIME_UNSET) {
secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset);
}
}
}
return earliestSamplePosition;
SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset);
if (secondTimeUs == C.TIME_UNSET) {
return new SeekPoints(firstSeekPoint);
} else {
SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset);
return new SeekPoints(firstSeekPoint, secondSeekPoint);
}
}
// Private methods.
@ -326,31 +363,11 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
/**
* Process an ftyp atom to determine whether the media is QuickTime.
*
* @param atomData The ftyp atom data.
* @return Whether the media is QuickTime.
*/
private static boolean processFtypAtom(ParsableByteArray atomData) {
atomData.setPosition(Atom.HEADER_SIZE);
int majorBrand = atomData.readInt();
if (majorBrand == BRAND_QUICKTIME) {
return true;
}
atomData.skipBytes(4); // minor_version
while (atomData.bytesLeft() > 0) {
if (atomData.readInt() == BRAND_QUICKTIME) {
return true;
}
}
return false;
}
/**
* Updates the stored track metadata to reflect the contents of the specified moov atom.
*/
private void processMoovAtom(ContainerAtom moov) throws ParserException {
int firstVideoTrackIndex = C.INDEX_UNSET;
long durationUs = C.TIME_UNSET;
List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE;
@ -402,6 +419,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
mp4Track.trackOutput.format(format);
durationUs = Math.max(durationUs, track.durationUs);
if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
firstVideoTrackIndex = tracks.size();
}
tracks.add(mp4Track);
long firstSampleOffset = trackSampleTable.offsets[0];
@ -409,8 +429,10 @@ public final class Mp4Extractor implements Extractor, SeekMap {
earliestSampleOffset = firstSampleOffset;
}
}
this.firstVideoTrackIndex = firstVideoTrackIndex;
this.durationUs = durationUs;
this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
extractorOutput.endTracks();
extractorOutput.seekMap(this);
}
@ -538,6 +560,66 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
/**
* Adjusts a seek point offset to take into account the track with the given {@code sampleTable},
* for a given {@code seekTimeUs}.
*
* @param sampleTable The sample table to use.
* @param seekTimeUs The seek time in microseconds.
* @param offset The current offset.
* @return The adjusted offset.
*/
private static long maybeAdjustSeekOffset(
TrackSampleTable sampleTable, long seekTimeUs, long offset) {
int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs);
if (sampleIndex == C.INDEX_UNSET) {
return offset;
}
long sampleOffset = sampleTable.offsets[sampleIndex];
return Math.min(sampleOffset, offset);
}
/**
* Returns the index of the synchronization sample before or at {@code timeUs}, or the index of
* the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if
* there are no synchronization samples in the table.
*
* @param sampleTable The sample table in which to locate a synchronization sample.
* @param timeUs A time in microseconds.
* @return The index of the synchronization sample before or at {@code timeUs}, or the index of
* the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET}
* if there are no synchronization samples in the table.
*/
private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) {
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
if (sampleIndex == C.INDEX_UNSET) {
// Handle the case where the requested time is before the first synchronization sample.
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
}
return sampleIndex;
}
/**
* Process an ftyp atom to determine whether the media is QuickTime.
*
* @param atomData The ftyp atom data.
* @return Whether the media is QuickTime.
*/
private static boolean processFtypAtom(ParsableByteArray atomData) {
atomData.setPosition(Atom.HEADER_SIZE);
int majorBrand = atomData.readInt();
if (majorBrand == BRAND_QUICKTIME) {
return true;
}
atomData.skipBytes(4); // minor_version
while (atomData.bytesLeft() > 0) {
if (atomData.readInt() == BRAND_QUICKTIME) {
return true;
}
}
return false;
}
/**
* Returns whether the extractor should decode a leaf atom with type {@code atom}.
*/

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions;
import java.io.EOFException;
import java.io.IOException;
@ -219,12 +220,13 @@ import java.io.IOException;
}
@Override
public long getPosition(long timeUs) {
public SeekPoints getSeekPoints(long timeUs) {
if (timeUs == 0) {
return startPosition;
return new SeekPoints(new SeekPoint(0, startPosition));
}
long granule = streamReader.convertTimeToGranule(timeUs);
return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
}
@Override

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
@ -192,10 +193,20 @@ import java.util.List;
}
@Override
public long getPosition(long timeUs) {
public SeekPoints getSeekPoints(long timeUs) {
long granule = convertTimeToGranule(timeUs);
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
return firstFrameOffset + seekPointOffsets[index];
long seekTimeUs = convertGranuleToTime(seekPointGranules[index]);
long seekPosition = firstFrameOffset + seekPointOffsets[index];
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) {
return new SeekPoints(seekPoint);
} else {
long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]);
long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1];
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint);
}
}
@Override

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util;
/** Header for a WAV file. */
@ -83,13 +84,22 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
public long getPosition(long timeUs) {
public SeekPoints getSeekPoints(long timeUs) {
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);
// Add data start position.
return dataStartPosition + positionOffset;
long seekPosition = dataStartPosition + positionOffset;
long seekTimeUs = getTimeUs(seekPosition);
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) {
return new SeekPoints(seekPoint);
} else {
long secondSeekPosition = seekPosition + blockAlignment;
long secondSeekTimeUs = getTimeUs(secondSeekPosition);
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint);
}
}
// Misc getters.
@ -100,7 +110,8 @@ import com.google.android.exoplayer2.util.Util;
* @param position The position in bytes.
*/
public long getTimeUs(long position) {
return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
long positionOffset = Math.max(0, position - dataStartPosition);
return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond;
}
/** Returns the bytes per frame of this WAV. */

View file

@ -549,7 +549,8 @@ import java.util.Arrays;
pendingResetPositionUs = C.TIME_UNSET;
return;
}
loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs);
loadable.setLoadPosition(
seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs);
pendingResetPositionUs = C.TIME_UNSET;
}
extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();

View file

@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import org.junit.Before;
@ -92,27 +94,39 @@ public final class XingSeekerTest {
}
@Test
public void testGetPositionAtStartOfStream() {
assertThat(seeker.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize);
assertThat(seekerWithInputLength.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize);
public void testGetSeekPointsAtStartOfStream() {
SeekPoints seekPoints = seeker.getSeekPoints(0);
SeekPoint seekPoint = seekPoints.first;
assertThat(seekPoint).isEqualTo(seekPoints.second);
assertThat(seekPoint.timeUs).isEqualTo(0);
assertThat(seekPoint.position).isEqualTo(XING_FRAME_POSITION + xingFrameSize);
}
@Test
public void testGetPositionAtEndOfStream() {
assertThat(seeker.getPosition(STREAM_DURATION_US))
.isEqualTo(STREAM_LENGTH - 1);
assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US))
.isEqualTo(STREAM_LENGTH - 1);
public void testGetSeekPointsAtEndOfStream() {
SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US);
SeekPoint seekPoint = seekPoints.first;
assertThat(seekPoint).isEqualTo(seekPoints.second);
assertThat(seekPoint.timeUs).isEqualTo(STREAM_DURATION_US);
assertThat(seekPoint.position).isEqualTo(STREAM_LENGTH - 1);
}
@Test
public void testGetTimeForAllPositions() {
for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) {
int position = XING_FRAME_POSITION + offset;
// Test seeker.
long timeUs = seeker.getTimeUs(position);
assertThat(seeker.getPosition(timeUs)).isEqualTo(position);
SeekPoints seekPoints = seeker.getSeekPoints(timeUs);
SeekPoint seekPoint = seekPoints.first;
assertThat(seekPoint).isEqualTo(seekPoints.second);
assertThat(seekPoint.position).isEqualTo(position);
// Test seekerWithInputLength.
timeUs = seekerWithInputLength.getTimeUs(position);
assertThat(seekerWithInputLength.getPosition(timeUs)).isEqualTo(position);
seekPoints = seekerWithInputLength.getSeekPoints(timeUs);
seekPoint = seekPoints.first;
assertThat(seekPoint).isEqualTo(seekPoints.second);
assertThat(seekPoint.position).isEqualTo(position);
}
}

View file

@ -143,7 +143,7 @@ public final class ExtractorAsserts {
long durationUs = seekMap.getDurationUs();
for (int j = 0; j < 4; j++) {
long timeUs = (durationUs * j) / 3;
long position = seekMap.getPosition(timeUs);
long position = seekMap.getSeekPoints(timeUs).first.position;
input.setPosition((int) position);
for (int i = 0; i < extractorOutput.numberOfTracks; i++) {
extractorOutput.trackOutputs.valueAt(i).clear();

View file

@ -78,7 +78,7 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
Assert.assertNotNull(seekMap);
Assert.assertEquals(expected.seekMap.getClass(), seekMap.getClass());
Assert.assertEquals(expected.seekMap.isSeekable(), seekMap.isSeekable());
Assert.assertEquals(expected.seekMap.getPosition(0), seekMap.getPosition(0));
Assert.assertEquals(expected.seekMap.getSeekPoints(0), seekMap.getSeekPoints(0));
}
for (int i = 0; i < numberOfTracks; i++) {
Assert.assertEquals(expected.trackOutputs.keyAt(i), trackOutputs.keyAt(i));
@ -114,10 +114,11 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
@Override
public void dump(Dumper dumper) {
if (seekMap != null) {
dumper.startBlock("seekMap")
dumper
.startBlock("seekMap")
.add("isSeekable", seekMap.isSeekable())
.addTime("duration", seekMap.getDurationUs())
.add("getPosition(0)", seekMap.getPosition(0))
.add("getPosition(0)", seekMap.getSeekPoints(0))
.endBlock();
}
dumper.add("numberOfTracks", numberOfTracks);