Commit eb4920bb by Oliver Woodman

Better compatibility with MKV test streams.

1. Fix seeking in test2.mkv by handling non-default timescale
   after duration.
2. Fix handling of missing cues in test6.mkv by allowing playback
   to continue (but all seeks will reset to t=0).

Issue #631
parent 7bc1241e
......@@ -21,6 +21,23 @@ package com.google.android.exoplayer.extractor;
public interface SeekMap {
/**
* A {@link SeekMap} that does not support seeking.
*/
public static final SeekMap UNSEEKABLE = new SeekMap() {
@Override
public boolean isSeekable() {
return false;
}
@Override
public long getPosition(long timeUs) {
return 0;
}
};
/**
* Whether or not the seeking is supported.
* <p>
* If seeking is not supported then the only valid seek position is the start of the file, and so
......
......@@ -28,7 +28,7 @@ import java.io.IOException;
* Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
* headers.
*/
public class AdtsExtractor implements Extractor, SeekMap {
public class AdtsExtractor implements Extractor {
private static final int MAX_PACKET_SIZE = 200;
......@@ -53,7 +53,7 @@ public class AdtsExtractor implements Extractor, SeekMap {
public void init(ExtractorOutput output) {
adtsReader = new AdtsReader(output.track(0));
output.endTracks();
output.seekMap(this);
output.seekMap(SeekMap.UNSEEKABLE);
}
@Override
......@@ -81,16 +81,4 @@ public class AdtsExtractor implements Extractor, SeekMap {
return RESULT_CONTINUE;
}
// SeekMap implementation.
@Override
public boolean isSeekable() {
return false;
}
@Override
public long getPosition(long timeUs) {
return 0;
}
}
......@@ -34,7 +34,7 @@ import java.io.IOException;
/**
* Facilitates the extraction of data from the MPEG-2 TS container format.
*/
public final class TsExtractor implements Extractor, SeekMap {
public final class TsExtractor implements Extractor {
private static final String TAG = "TsExtractor";
......@@ -98,7 +98,7 @@ public final class TsExtractor implements Extractor, SeekMap {
@Override
public void init(ExtractorOutput output) {
this.output = output;
output.seekMap(this);
output.seekMap(SeekMap.UNSEEKABLE);
}
@Override
......@@ -153,18 +153,6 @@ public final class TsExtractor implements Extractor, SeekMap {
return RESULT_CONTINUE;
}
// SeekMap implementation.
@Override
public boolean isSeekable() {
return false;
}
@Override
public long getPosition(long timeUs) {
return 0;
}
// Internals.
/**
......
......@@ -24,12 +24,14 @@ import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import android.util.Pair;
......@@ -39,7 +41,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* An extractor to facilitate data retrieval from the WebM container format.
......@@ -83,6 +84,7 @@ public final class WebmExtractor implements Extractor {
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_SEGMENT_INFO = 0x1549A966;
private static final int ID_SEEK_HEAD = 0x114D9B74;
private static final int ID_SEEK = 0x4DBB;
private static final int ID_SEEK_ID = 0x53AB;
......@@ -147,7 +149,8 @@ public final class WebmExtractor implements Extractor {
private long segmentContentPosition = UNKNOWN;
private long segmentContentSize = UNKNOWN;
private long timecodeScale = 1000000L;
private long timecodeScale = C.UNKNOWN_TIME_US;
private long durationTimecode = C.UNKNOWN_TIME_US;
private long durationUs = C.UNKNOWN_TIME_US;
private TrackFormat trackFormat; // Used to store the last seen track.
......@@ -320,10 +323,17 @@ public final class WebmExtractor implements Extractor {
seenClusterPositionForCurrentCuePoint = false;
return;
case ID_CLUSTER:
// If we encounter a Cluster before building Cues, then we should try to build cues first
// before parsing the Cluster.
if (cuesState == CUES_STATE_NOT_BUILT && cuesContentPosition != UNKNOWN) {
seekForCues = true;
if (cuesState == CUES_STATE_NOT_BUILT) {
// We need to build cues before parsing the cluster.
if (cuesContentPosition != UNKNOWN) {
// We know where the Cues element is located. Seek to request it.
seekForCues = true;
} else {
// We don't know where the Cues element is located. It's most likely omitted. Allow
// playback, but disable seeking.
extractorOutput.seekMap(SeekMap.UNSEEKABLE);
cuesState = CUES_STATE_BUILT;
}
}
return;
case ID_BLOCK_GROUP:
......@@ -345,6 +355,15 @@ public final class WebmExtractor implements Extractor {
/* package */ void endMasterElement(int id) throws ParserException {
switch (id) {
case ID_SEGMENT_INFO:
if (timecodeScale == C.UNKNOWN_TIME_US) {
// timecodeScale was omitted. Use the default value.
timecodeScale = 1000000;
}
if (durationTimecode != C.UNKNOWN_TIME_US) {
durationUs = scaleTimecodeToUs(durationTimecode);
}
return;
case ID_SEEK:
if (seekEntryId == UNKNOWN || seekEntryPosition == UNKNOWN) {
throw new ParserException("Mandatory element SeekID or SeekPosition not found");
......@@ -355,7 +374,7 @@ public final class WebmExtractor implements Extractor {
return;
case ID_CUES:
if (cuesState != CUES_STATE_BUILT) {
extractorOutput.seekMap(buildCues());
extractorOutput.seekMap(buildSeekMap());
cuesState = CUES_STATE_BUILT;
} else {
// We have already built the cues. Ignore.
......@@ -528,7 +547,7 @@ public final class WebmExtractor implements Extractor {
/* package */ void floatElement(int id, double value) {
switch (id) {
case ID_DURATION:
durationUs = scaleTimecodeToUs((long) value);
durationTimecode = (long) value;
return;
case ID_SAMPLING_FREQUENCY:
trackFormat.sampleRate = (int) value;
......@@ -865,19 +884,19 @@ public final class WebmExtractor implements Extractor {
}
/**
* Builds a {@link ChunkIndex} containing recently gathered Cues information.
* Builds a {@link SeekMap} from the recently gathered Cues information.
*
* @return The built {@link ChunkIndex}.
* @throws ParserException If the index could not be built.
* @return The built {@link SeekMap}. May be {@link SeekMap#UNSEEKABLE} if cues information was
* missing or incomplete.
*/
private ChunkIndex buildCues() throws ParserException {
if (segmentContentPosition == UNKNOWN) {
throw new ParserException("Segment start/end offsets unknown");
} else if (durationUs == C.UNKNOWN_TIME_US) {
throw new ParserException("Duration unknown");
} else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new ParserException("Invalid/missing cue points");
private SeekMap buildSeekMap() {
if (segmentContentPosition == UNKNOWN || durationUs == C.UNKNOWN_TIME_US
|| cueTimesUs == null || cueTimesUs.size() == 0
|| cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) {
// Cues information is missing or incomplete.
cueTimesUs = null;
cueClusterPositions = null;
return SeekMap.UNSEEKABLE;
}
int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize];
......@@ -927,8 +946,11 @@ public final class WebmExtractor implements Extractor {
return false;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException {
if (timecodeScale == C.UNKNOWN_TIME_US) {
throw new ParserException("Can't scale timecode prior to timecodeScale being set.");
}
return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000);
}
private static boolean isCodecSupported(String codecId) {
......@@ -984,7 +1006,7 @@ public final class WebmExtractor implements Extractor {
}
@Override
public void floatElement(int id, double value) {
public void floatElement(int id, double value) throws ParserException {
WebmExtractor.this.floatElement(id, value);
}
......
......@@ -80,8 +80,14 @@ import java.util.List;
return this;
}
public StreamBuilder setInfo(int timecodeScale, long durationUs) {
info = createInfoElement(timecodeScale, durationUs);
public StreamBuilder setInfo(int timecodeScale, long durationTimecode) {
return setInfo(timecodeScale, durationTimecode, false, false);
}
public StreamBuilder setInfo(int timecodeScale, long durationTimecode,
boolean omitTimecodeScaleIfDefault, boolean durationFirst) {
info = createInfoElement(timecodeScale, durationTimecode, omitTimecodeScaleIfDefault,
durationFirst);
return this;
}
......@@ -177,28 +183,38 @@ import java.util.List;
Assertions.checkNotNull(info);
EbmlElement tracks = element(0x1654AE6B, trackEntries.toArray(new EbmlElement[0]));
EbmlElement[] children;
if (cuePointCount == 0) {
children = new EbmlElement[2 + mediaSegments.size()];
System.arraycopy(mediaSegments.toArray(new EbmlElement[0]), 0, children, 2,
mediaSegments.size());
children[0] = info;
children[1] = tracks;
} else {
// Get the size of the initialization segment.
EbmlElement[] cuePointElements = new EbmlElement[cuePointCount];
for (int i = 0; i < cuePointCount; i++) {
cuePointElements[i] = createCuePointElement(10 * i, 0);
}
EbmlElement cues = element(0x1C53BB6B, cuePointElements); // Cues
long initializationSegmentSize = info.getSize() + tracks.getSize() + cues.getSize();
// Get the size of the initialization segment.
EbmlElement[] cuePointElements = new EbmlElement[cuePointCount];
for (int i = 0; i < cuePointCount; i++) {
cuePointElements[i] = createCuePointElement(10 * i, 0);
// Recreate the initialization segment using its size as an offset.
for (int i = 0; i < cuePointCount; i++) {
cuePointElements[i] = createCuePointElement(10 * i, (int) initializationSegmentSize);
}
cues = element(0x1C53BB6B, cuePointElements); // Cues
// Build the top-level segment element.
children = new EbmlElement[3 + mediaSegments.size()];
System.arraycopy(mediaSegments.toArray(new EbmlElement[0]), 0, children, 3,
mediaSegments.size());
children[0] = info;
children[1] = tracks;
children[2] = cues;
}
EbmlElement cues = element(0x1C53BB6B, cuePointElements); // Cues
long initializationSegmentSize = info.getSize() + tracks.getSize() + cues.getSize();
// Recreate the initialization segment using its size as an offset.
for (int i = 0; i < cuePointCount; i++) {
cuePointElements[i] = createCuePointElement(10 * i, (int) initializationSegmentSize);
}
cues = element(0x1C53BB6B, cuePointElements); // Cues
// Build the top-level segment element.
EbmlElement[] children = new EbmlElement[3 + mediaSegments.size()];
System.arraycopy(mediaSegments.toArray(new EbmlElement[0]), 0, children, 3,
mediaSegments.size());
children[0] = info;
children[1] = tracks;
children[2] = cues;
EbmlElement segmentElement = element(0x18538067, children); // Segment
// Serialize the EBML header and the top-level segment element.
......@@ -221,12 +237,19 @@ import java.util.List;
element(0x4285, (byte) (docTypeReadVersion & 0xFF))); // DocTypeReadVersion
}
private EbmlElement createInfoElement(int timecodeScale, long durationUs) {
private EbmlElement createInfoElement(int timecodeScale, long durationTimecode,
boolean durationFirst, boolean omitDefaultTimecodeScale) {
byte[] timecodeScaleBytes = getIntegerBytes(timecodeScale);
byte[] durationBytes = getLongBytes(Double.doubleToLongBits(durationUs / 1000.0));
byte[] durationBytes = getLongBytes(Double.doubleToLongBits(durationTimecode));
EbmlElement durationElement = element(0x4489, durationBytes);
EbmlElement timescaleElement = element(0x2AD7B1, timecodeScaleBytes);
if (omitDefaultTimecodeScale && timecodeScale == 1000000) {
return element(0x1549A966, // Info
durationElement);
}
return element(0x1549A966, // Info
element(0x2AD7B1, timecodeScaleBytes), // TimecodeScale
element(0x4489, durationBytes)); // Duration
durationFirst ? durationElement : timescaleElement,
durationFirst ? timescaleElement : durationElement);
}
private static EbmlElement createVideoTrackEntry(String codecId, int pixelWidth, int pixelHeight,
......
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