Commit fbfa43f5 by olly Committed by Oliver Woodman

Enhance SeekMaps to return SeekPoints

Issue: #2882

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177814974
parent bb0fae3e
Showing with 439 additions and 173 deletions
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8880 getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8880 getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8880 getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8880 getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; 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.TrackOutput;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor { ...@@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor {
} }
metadataParsed = true; metadataParsed = true;
extractorOutput.seekMap(new SeekMap() { boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
final boolean isSeekable = decoderJni.getSeekPosition(0) != -1; extractorOutput.seekMap(
final long durationUs = streamInfo.durationUs(); isSeekable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
@Override : new SeekMap.Unseekable(streamInfo.durationUs(), 0));
public boolean isSeekable() {
return isSeekable;
}
@Override
public long getPosition(long timeUs) {
return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0;
}
@Override
public long getDurationUs() {
return durationUs;
}
});
Format mediaFormat = Format mediaFormat =
Format.createAudioSampleFormat( Format.createAudioSampleFormat(
null, null,
...@@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor { ...@@ -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;
}
}
} }
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = 1136000 duration = 1136000
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2 numberOfTracks = 2
track 8: track 8:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1072000 duration = 1072000
getPosition(0) = 5576 getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2 numberOfTracks = 2
track 1: track 1:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1072000 duration = 1072000
getPosition(0) = 5576 getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2 numberOfTracks = 2
track 1: track 1:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1072000 duration = 1072000
getPosition(0) = 5576 getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2 numberOfTracks = 2
track 1: track 1:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1072000 duration = 1072000
getPosition(0) = 5576 getPosition(0) = [[timeUs=67000, position=5576]]
numberOfTracks = 2 numberOfTracks = 2
track 1: track 1:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = 1000 duration = 1000
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 1: track 1:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = 1000 duration = 1000
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 1: track 1:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2784000 duration = 2784000
getPosition(0) = 201 getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2784000 duration = 2784000
getPosition(0) = 201 getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2784000 duration = 2784000
getPosition(0) = 201 getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2784000 duration = 2784000
getPosition(0) = 201 getPosition(0) = [[timeUs=0, position=201]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26125
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26125
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26125
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26125
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1024000 duration = 1024000
getPosition(0) = 48 getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2 numberOfTracks = 2
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1024000 duration = 1024000
getPosition(0) = 48 getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2 numberOfTracks = 2
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1024000 duration = 1024000
getPosition(0) = 48 getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2 numberOfTracks = 2
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1024000 duration = 1024000
getPosition(0) = 48 getPosition(0) = [[timeUs=0, position=48]]
numberOfTracks = 2 numberOfTracks = 2
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 1828 getPosition(0) = [[timeUs=0, position=1828]]
numberOfTracks = 2 numberOfTracks = 2
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 1828 getPosition(0) = [[timeUs=0, position=1828]]
numberOfTracks = 3 numberOfTracks = 3
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2747500 duration = 2747500
getPosition(0) = 125 getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2747500 duration = 2747500
getPosition(0) = 125 getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2747500 duration = 2747500
getPosition(0) = 125 getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2747500 duration = 2747500
getPosition(0) = 125 getPosition(0) = [[timeUs=0, position=125]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8457 getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8457 getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8457 getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8457 getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8457 getPosition(0) = [[timeUs=0, position=8457]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8407 getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8407 getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8407 getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 8407 getPosition(0) = [[timeUs=0, position=8407]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 3995 getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 3995 getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 3995 getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 2741000 duration = 2741000
getPosition(0) = 3995 getPosition(0) = [[timeUs=0, position=3995]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2 numberOfTracks = 2
track 0: track 0:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2 numberOfTracks = 2
track 192: track 192:
format: format:
......
seekMap: seekMap:
isSeekable = false isSeekable = false
duration = UNSET TIME duration = UNSET TIME
getPosition(0) = 0 getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 2 numberOfTracks = 2
track 256: track 256:
format: format:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1000000 duration = 1000000
getPosition(0) = 78 getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
...@@ -27,15 +27,15 @@ track 0: ...@@ -27,15 +27,15 @@ track 0:
initializationData: initializationData:
sample count = 3 sample count = 3
sample 0: sample 0:
time = 884 time = 0
flags = 1 flags = 1
data = length 32768, hash 9A8CEEBA data = length 32768, hash 9A8CEEBA
sample 1: sample 1:
time = 372403 time = 371519
flags = 1 flags = 1
data = length 32768, hash C1717317 data = length 32768, hash C1717317
sample 2: sample 2:
time = 743922 time = 743038
flags = 1 flags = 1
data = length 22664, hash 819F5F62 data = length 22664, hash 819F5F62
tracksEnded = true tracksEnded = true
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1000000 duration = 1000000
getPosition(0) = 78 getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
...@@ -27,11 +27,11 @@ track 0: ...@@ -27,11 +27,11 @@ track 0:
initializationData: initializationData:
sample count = 2 sample count = 2
sample 0: sample 0:
time = 334195 time = 333310
flags = 1 flags = 1
data = length 32768, hash 42D6E860 data = length 32768, hash 42D6E860
sample 1: sample 1:
time = 705714 time = 704829
flags = 1 flags = 1
data = length 26034, hash 62692C38 data = length 26034, hash 62692C38
tracksEnded = true tracksEnded = true
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1000000 duration = 1000000
getPosition(0) = 78 getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
...@@ -27,7 +27,7 @@ track 0: ...@@ -27,7 +27,7 @@ track 0:
initializationData: initializationData:
sample count = 1 sample count = 1
sample 0: sample 0:
time = 667528 time = 666643
flags = 1 flags = 1
data = length 29402, hash 4241604E data = length 29402, hash 4241604E
tracksEnded = true tracksEnded = true
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 1000000 duration = 1000000
getPosition(0) = 78 getPosition(0) = [[timeUs=0, position=78]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
format: format:
...@@ -27,7 +27,7 @@ track 0: ...@@ -27,7 +27,7 @@ track 0:
initializationData: initializationData:
sample count = 1 sample count = 1
sample 0: sample 0:
time = 1000861 time = 999977
flags = 1 flags = 1
data = length 2, hash 116 data = length 2, hash 116
tracksEnded = true tracksEnded = true
...@@ -91,8 +91,15 @@ public final class ChunkIndex implements SeekMap { ...@@ -91,8 +91,15 @@ public final class ChunkIndex implements SeekMap {
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
return offsets[getChunkIndex(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);
}
} }
} }
...@@ -16,36 +16,36 @@ ...@@ -16,36 +16,36 @@
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C; 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. * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
*/ */
public interface SeekMap { public interface SeekMap {
/** /** A {@link SeekMap} that does not support seeking. */
* A {@link SeekMap} that does not support seeking.
*/
final class Unseekable implements SeekMap { final class Unseekable implements SeekMap {
private final long durationUs; 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 * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
* the duration is unknown. * duration is unknown.
*/ */
public Unseekable(long durationUs) { public Unseekable(long durationUs) {
this(durationUs, 0); this(durationUs, 0);
} }
/** /**
* @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
* the duration is unknown. * duration is unknown.
* @param startPosition The position (byte offset) of the start of the media. * @param startPosition The position (byte offset) of the start of the media.
*/ */
public Unseekable(long durationUs, long startPosition) { public Unseekable(long durationUs, long startPosition) {
this.durationUs = durationUs; this.durationUs = durationUs;
this.startPosition = startPosition; startSeekPoints =
new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition));
} }
@Override @Override
...@@ -59,17 +59,58 @@ public interface SeekMap { ...@@ -59,17 +59,58 @@ public interface SeekMap {
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
return startPosition; 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. * 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. * @return Whether seeking is supported.
*/ */
...@@ -78,20 +119,22 @@ public interface SeekMap { ...@@ -78,20 +119,22 @@ public interface SeekMap {
/** /**
* Returns the duration of the stream in microseconds. * Returns the duration of the stream in microseconds.
* *
* @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is
* duration is unknown. * unknown.
*/ */
long getDurationUs(); long getDurationUs();
/** /**
* Maps a seek position in microseconds to a corresponding position (byte offset) in the stream * Obtains seek points for the specified seek time in microseconds. The returned {@link
* from which data can be provided to the extractor. * SeekPoints} will contain one or two distinct seek points.
* *
* @param timeUs A seek position in microseconds. * <p>Two seek points [A, B] are returned in the case that seeking can only be performed to
* @return The corresponding position (byte offset) in the stream from which data can be provided * discrete points in time, there does not exist a seek point at exactly the requested time, and
* to the extractor. If {@link #isSeekable()} returns false then the returned value will be * there exist seek points on both sides of it. In this case A and B are the closest seek points
* independent of {@code timeUs}, and will indicate the start of the media in the stream. * 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);
} }
/*
* 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;
}
}
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3; ...@@ -17,6 +17,7 @@ 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.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
...@@ -57,16 +58,25 @@ import com.google.android.exoplayer2.util.Util; ...@@ -57,16 +58,25 @@ import com.google.android.exoplayer2.util.Util;
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
if (dataSize == C.LENGTH_UNSET) { 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); long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
// Constrain to nearest preceding frame offset. // Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / frameSize) * frameSize; positionOffset = (positionOffset / frameSize) * frameSize;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize);
// Add data start position. long seekPosition = firstFramePosition + positionOffset;
return 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
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
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.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -106,8 +107,15 @@ import com.google.android.exoplayer2.util.Util; ...@@ -106,8 +107,15 @@ import com.google.android.exoplayer2.util.Util;
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)]; 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 @Override
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
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.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -107,10 +108,11 @@ import com.google.android.exoplayer2.util.Util; ...@@ -107,10 +108,11 @@ import com.google.android.exoplayer2.util.Util;
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
if (!isSeekable()) { 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 percent = (timeUs * 100d) / durationUs;
double scaledPosition; double scaledPosition;
if (percent <= 0) { if (percent <= 0) {
...@@ -129,7 +131,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -129,7 +131,7 @@ import com.google.android.exoplayer2.util.Util;
long positionOffset = Math.round((scaledPosition / 256) * dataSize); long positionOffset = Math.round((scaledPosition / 256) * dataSize);
// Ensure returned positions skip the frame containing the XING header. // Ensure returned positions skip the frame containing the XING header.
positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
return dataStartPosition + positionOffset; return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset));
} }
@Override @Override
......
...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; ...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; 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.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
...@@ -108,6 +109,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -108,6 +109,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private Mp4Track[] tracks; private Mp4Track[] tracks;
private int firstVideoTrackIndex;
private long durationUs; private long durationUs;
private boolean isQuickTime; private boolean isQuickTime;
...@@ -196,21 +198,56 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -196,21 +198,56 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
long earliestSamplePosition = Long.MAX_VALUE; if (tracks.length == 0) {
for (Mp4Track track : tracks) { return new SeekPoints(SeekPoint.START);
TrackSampleTable sampleTable = track.sampleTable; }
int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
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) { if (sampleIndex == C.INDEX_UNSET) {
// Handle the case where the requested time is before the first synchronization sample. return new SeekPoints(SeekPoint.START);
sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
} }
long offset = sampleTable.offsets[sampleIndex]; long sampleTimeUs = sampleTable.timestampsUs[sampleIndex];
if (offset < earliestSamplePosition) { firstTimeUs = sampleTimeUs;
earliestSamplePosition = offset; 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);
}
}
}
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);
} }
return earliestSamplePosition;
} }
// Private methods. // Private methods.
...@@ -327,30 +364,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -327,30 +364,10 @@ 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. * Updates the stored track metadata to reflect the contents of the specified moov atom.
*/ */
private void processMoovAtom(ContainerAtom moov) throws ParserException { private void processMoovAtom(ContainerAtom moov) throws ParserException {
int firstVideoTrackIndex = C.INDEX_UNSET;
long durationUs = C.TIME_UNSET; long durationUs = C.TIME_UNSET;
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
long earliestSampleOffset = Long.MAX_VALUE; long earliestSampleOffset = Long.MAX_VALUE;
...@@ -402,6 +419,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -402,6 +419,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
mp4Track.trackOutput.format(format); mp4Track.trackOutput.format(format);
durationUs = Math.max(durationUs, track.durationUs); durationUs = Math.max(durationUs, track.durationUs);
if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
firstVideoTrackIndex = tracks.size();
}
tracks.add(mp4Track); tracks.add(mp4Track);
long firstSampleOffset = trackSampleTable.offsets[0]; long firstSampleOffset = trackSampleTable.offsets[0];
...@@ -409,8 +429,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -409,8 +429,10 @@ public final class Mp4Extractor implements Extractor, SeekMap {
earliestSampleOffset = firstSampleOffset; earliestSampleOffset = firstSampleOffset;
} }
} }
this.firstVideoTrackIndex = firstVideoTrackIndex;
this.durationUs = durationUs; this.durationUs = durationUs;
this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
extractorOutput.endTracks(); extractorOutput.endTracks();
extractorOutput.seekMap(this); extractorOutput.seekMap(this);
} }
...@@ -539,6 +561,66 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -539,6 +561,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}. * Returns whether the extractor should decode a leaf atom with type {@code atom}.
*/ */
private static boolean shouldParseLeafAtom(int atom) { private static boolean shouldParseLeafAtom(int atom) {
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
...@@ -219,12 +220,13 @@ import java.io.IOException; ...@@ -219,12 +220,13 @@ import java.io.IOException;
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
if (timeUs == 0) { if (timeUs == 0) {
return startPosition; return new SeekPoints(new SeekPoint(0, startPosition));
} }
long granule = streamReader.convertTimeToGranule(timeUs); 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 @Override
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; 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.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -192,10 +193,20 @@ import java.util.List; ...@@ -192,10 +193,20 @@ import java.util.List;
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
long granule = convertTimeToGranule(timeUs); long granule = convertTimeToGranule(timeUs);
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); 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 @Override
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** Header for a WAV file. */ /** Header for a WAV file. */
...@@ -83,13 +84,22 @@ import com.google.android.exoplayer2.util.Util; ...@@ -83,13 +84,22 @@ import com.google.android.exoplayer2.util.Util;
} }
@Override @Override
public long getPosition(long timeUs) { public SeekPoints getSeekPoints(long timeUs) {
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Constrain to nearest preceding frame offset. // Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / blockAlignment) * blockAlignment; positionOffset = (positionOffset / blockAlignment) * blockAlignment;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);
// Add data start position. long seekPosition = dataStartPosition + positionOffset;
return 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. // Misc getters.
...@@ -100,7 +110,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -100,7 +110,8 @@ import com.google.android.exoplayer2.util.Util;
* @param position The position in bytes. * @param position The position in bytes.
*/ */
public long getTimeUs(long position) { 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. */ /** Returns the bytes per frame of this WAV. */
......
...@@ -549,7 +549,8 @@ import java.util.Arrays; ...@@ -549,7 +549,8 @@ import java.util.Arrays;
pendingResetPositionUs = C.TIME_UNSET; pendingResetPositionUs = C.TIME_UNSET;
return; return;
} }
loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs); loadable.setLoadPosition(
seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs);
pendingResetPositionUs = C.TIME_UNSET; pendingResetPositionUs = C.TIME_UNSET;
} }
extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
......
...@@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; 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.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import org.junit.Before; import org.junit.Before;
...@@ -92,27 +94,39 @@ public final class XingSeekerTest { ...@@ -92,27 +94,39 @@ public final class XingSeekerTest {
} }
@Test @Test
public void testGetPositionAtStartOfStream() { public void testGetSeekPointsAtStartOfStream() {
assertThat(seeker.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); SeekPoints seekPoints = seeker.getSeekPoints(0);
assertThat(seekerWithInputLength.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); SeekPoint seekPoint = seekPoints.first;
assertThat(seekPoint).isEqualTo(seekPoints.second);
assertThat(seekPoint.timeUs).isEqualTo(0);
assertThat(seekPoint.position).isEqualTo(XING_FRAME_POSITION + xingFrameSize);
} }
@Test @Test
public void testGetPositionAtEndOfStream() { public void testGetSeekPointsAtEndOfStream() {
assertThat(seeker.getPosition(STREAM_DURATION_US)) SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US);
.isEqualTo(STREAM_LENGTH - 1); SeekPoint seekPoint = seekPoints.first;
assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) assertThat(seekPoint).isEqualTo(seekPoints.second);
.isEqualTo(STREAM_LENGTH - 1); assertThat(seekPoint.timeUs).isEqualTo(STREAM_DURATION_US);
assertThat(seekPoint.position).isEqualTo(STREAM_LENGTH - 1);
} }
@Test @Test
public void testGetTimeForAllPositions() { public void testGetTimeForAllPositions() {
for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) {
int position = XING_FRAME_POSITION + offset; int position = XING_FRAME_POSITION + offset;
// Test seeker.
long timeUs = seeker.getTimeUs(position); 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); 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);
} }
} }
......
...@@ -143,7 +143,7 @@ public final class ExtractorAsserts { ...@@ -143,7 +143,7 @@ public final class ExtractorAsserts {
long durationUs = seekMap.getDurationUs(); long durationUs = seekMap.getDurationUs();
for (int j = 0; j < 4; j++) { for (int j = 0; j < 4; j++) {
long timeUs = (durationUs * j) / 3; long timeUs = (durationUs * j) / 3;
long position = seekMap.getPosition(timeUs); long position = seekMap.getSeekPoints(timeUs).first.position;
input.setPosition((int) position); input.setPosition((int) position);
for (int i = 0; i < extractorOutput.numberOfTracks; i++) { for (int i = 0; i < extractorOutput.numberOfTracks; i++) {
extractorOutput.trackOutputs.valueAt(i).clear(); extractorOutput.trackOutputs.valueAt(i).clear();
......
...@@ -78,7 +78,7 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab ...@@ -78,7 +78,7 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
Assert.assertNotNull(seekMap); Assert.assertNotNull(seekMap);
Assert.assertEquals(expected.seekMap.getClass(), seekMap.getClass()); Assert.assertEquals(expected.seekMap.getClass(), seekMap.getClass());
Assert.assertEquals(expected.seekMap.isSeekable(), seekMap.isSeekable()); 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++) { for (int i = 0; i < numberOfTracks; i++) {
Assert.assertEquals(expected.trackOutputs.keyAt(i), trackOutputs.keyAt(i)); Assert.assertEquals(expected.trackOutputs.keyAt(i), trackOutputs.keyAt(i));
...@@ -114,10 +114,11 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab ...@@ -114,10 +114,11 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab
@Override @Override
public void dump(Dumper dumper) { public void dump(Dumper dumper) {
if (seekMap != null) { if (seekMap != null) {
dumper.startBlock("seekMap") dumper
.startBlock("seekMap")
.add("isSeekable", seekMap.isSeekable()) .add("isSeekable", seekMap.isSeekable())
.addTime("duration", seekMap.getDurationUs()) .addTime("duration", seekMap.getDurationUs())
.add("getPosition(0)", seekMap.getPosition(0)) .add("getPosition(0)", seekMap.getSeekPoints(0))
.endBlock(); .endBlock();
} }
dumper.add("numberOfTracks", numberOfTracks); dumper.add("numberOfTracks", numberOfTracks);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment