diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java index 9b9002f52b..9cedd0090d 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java @@ -46,8 +46,9 @@ public class DemoUtil { public static final int TYPE_HLS = 2; public static final int TYPE_MP4 = 3; public static final int TYPE_MP3 = 4; - public static final int TYPE_AAC = 5; - public static final int TYPE_OTHER = 6; + public static final int TYPE_TS = 5; + public static final int TYPE_AAC = 6; + public static final int TYPE_OTHER = 7; private static final CookieManager defaultCookieManager; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index a72df2f6d0..4b21b37bdc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer.demo.player.UnsupportedDrmException; import com.google.android.exoplayer.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer.extractor.ts.TsExtractor; import com.google.android.exoplayer.metadata.GeobMetadata; import com.google.android.exoplayer.metadata.PrivMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata; @@ -235,6 +236,9 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, case DemoUtil.TYPE_MP3: return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, new Mp3Extractor()); + case DemoUtil.TYPE_TS: + return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, + new TsExtractor()); case DemoUtil.TYPE_AAC: return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, new AdtsExtractor()); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index b407670056..420e7ec33c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -135,6 +135,9 @@ import java.util.Locale; new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/" + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", DemoUtil.TYPE_AAC), + new Sample("Apple TS 10s", "https://devimages.apple.com.edgekey.net/streaming/examples/" + + "bipbop_4x3/gear1/fileSequence0.ts", + DemoUtil.TYPE_TS), new Sample("Big Buck Bunny (MP4 Video)", "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube" + "&sparams=ip,ipbits,expire&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=" diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java index c11453c572..5c98a680e8 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Ac3Reader.java @@ -58,6 +58,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; state = STATE_FINDING_SYNC; } + @Override public void seek() { state = STATE_FINDING_SYNC; bytesRead = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java index 8bfe7e82b6..4e3a5b3c46 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsExtractor.java @@ -58,8 +58,8 @@ public class AdtsExtractor implements Extractor, SeekMap { @Override public void seek() { - adtsReader.seek(); firstPacket = true; + adtsReader.seek(); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java index 5f9af63643..fa4fd51dd2 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/AdtsReader.java @@ -62,12 +62,7 @@ import java.util.Collections; state = STATE_FINDING_SYNC; } - /** - * Notifies the reader that a seek has occurred. - *

- * The data passed to the next invocation of {@link #consume(ParsableByteArray, long, boolean)} - * should not be treated as a continuation of the data passed to previous calls. - */ + @Override public void seek() { state = STATE_FINDING_SYNC; bytesRead = 0; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java index d2df2aef14..7bcdd1775a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/ElementaryStreamReader.java @@ -32,6 +32,15 @@ import com.google.android.exoplayer.util.ParsableByteArray; this.output = output; } + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, long, boolean)} will not be a continuation of the data that + * was previously passed. Hence the reader should reset any internal state. + */ + public abstract void seek(); + /** * Consumes (possibly partial) payload data. * diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java index 4627cb6526..c8b023e8dc 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java @@ -63,22 +63,26 @@ import java.util.List; 2f }; + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // State that should be reset on seek. private final SeiReader seiReader; private final boolean[] prefixFlags; private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer sei; - private final ParsableByteArray seiWrapper; - - private boolean hasOutputFormat; - private int scratchEscapeCount; - private int[] scratchEscapePositions; - private boolean writingSample; + private long totalBytesWritten; + + // Per sample state that gets reset at the start of each sample. private boolean isKeyframe; private long samplePosition; private long sampleTimeUs; - private long totalBytesWritten; + + // Scratch variables to avoid allocations. + private final ParsableByteArray seiWrapper; + private int[] scratchEscapePositions; public H264Reader(TrackOutput output, SeiReader seiReader) { super(output); @@ -91,6 +95,17 @@ import java.util.List; scratchEscapePositions = new int[10]; } + @Override + public void seek() { + seiReader.seek(); + H264Util.clearPrefixFlags(prefixFlags); + sps.reset(); + pps.reset(); + sei.reset(); + writingSample = false; + totalBytesWritten = 0; + } + @Override public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { while (data.bytesLeft() > 0) { @@ -128,9 +143,9 @@ import java.util.List; writingSample = false; } writingSample = true; - samplePosition = totalBytesWritten - bytesWrittenPastNalUnit; - sampleTimeUs = pesTimeUs; isKeyframe = false; + sampleTimeUs = pesTimeUs; + samplePosition = totalBytesWritten - bytesWrittenPastNalUnit; } else if (nalUnitType == NAL_UNIT_TYPE_IDR) { isKeyframe = true; } @@ -317,7 +332,7 @@ import java.util.List; */ private int unescapeStream(byte[] data, int limit) { int position = 0; - scratchEscapeCount = 0; + int scratchEscapeCount = 0; while (position < limit) { position = findNextUnescapeIndex(data, position, limit); if (position < limit) { @@ -378,6 +393,17 @@ import java.util.List; nalData[3] = (byte) targetType; } + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + isCompleted = false; + } + + /** + * True if the buffer currently holds a complete NAL unit of the target type. + */ public boolean isCompleted() { return isCompleted; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java index e5a9d355ae..04fe15481e 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/Id3Reader.java @@ -25,7 +25,10 @@ import com.google.android.exoplayer.util.ParsableByteArray; */ /* package */ class Id3Reader extends ElementaryStreamReader { + // State that should be reset on seek. private boolean writingSample; + + // Per sample state that gets reset at the start of each sample. private long sampleTimeUs; private int sampleSize; @@ -34,6 +37,11 @@ import com.google.android.exoplayer.util.ParsableByteArray; output.format(MediaFormat.createId3Format()); } + @Override + public void seek() { + writingSample = false; + } + @Override public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) { if (startOfPacket) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java index c426f309b1..1bd169d544 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/SeiReader.java @@ -34,6 +34,11 @@ import com.google.android.exoplayer.util.ParsableByteArray; output.format(MediaFormat.createEia608Format()); } + @Override + public void seek() { + // Do nothing. + } + @Override public void consume(ParsableByteArray seiBuffer, long pesTimeUs, boolean startOfPacket) { // Skip the NAL prefix and type. diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 938e4afaf0..cde096dfad 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -20,6 +20,7 @@ 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.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; @@ -32,7 +33,7 @@ import java.io.IOException; /** * Facilitates the extraction of data from the MPEG-2 TS container format. */ -public final class TsExtractor implements Extractor { +public final class TsExtractor implements Extractor, SeekMap { private static final String TAG = "TsExtractor"; @@ -74,14 +75,21 @@ public final class TsExtractor implements Extractor { lastPts = Long.MIN_VALUE; } + // Extractor implementation. + @Override public void init(ExtractorOutput output) { this.output = output; + output.seekMap(this); } @Override public void seek() { - throw new UnsupportedOperationException(); + timestampOffsetUs = 0; + lastPts = Long.MIN_VALUE; + for (int i = 0; i < tsPayloadReaders.size(); i++) { + tsPayloadReaders.valueAt(i).seek(); + } } @Override @@ -125,6 +133,20 @@ public final class TsExtractor implements Extractor { return RESULT_CONTINUE; } + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return false; + } + + @Override + public long getPosition(long timeUs) { + return 0; + } + + // Internals. + /** * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. * @@ -157,6 +179,22 @@ public final class TsExtractor implements Extractor { */ private abstract static class TsPayloadReader { + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, boolean, ExtractorOutput)} will not be a continuation of + * the data that was previously passed. Hence the reader should reset any internal state. + */ + public abstract void seek(); + + /** + * Consumes the payload of a TS packet. + * + * @param data The TS packet. The position will be set to the start of the payload. + * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @param output The output to which parsed data should be written. + */ public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, ExtractorOutput output); @@ -173,6 +211,11 @@ public final class TsExtractor implements Extractor { patScratch = new ParsableBitArray(new byte[4]); } + @Override + public void seek() { + // Do nothing. + } + @Override public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, ExtractorOutput output) { @@ -213,6 +256,11 @@ public final class TsExtractor implements Extractor { pmtScratch = new ParsableBitArray(new byte[5]); } + @Override + public void seek() { + // Do nothing. + } + @Override public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, ExtractorOutput output) { @@ -311,9 +359,7 @@ public final class TsExtractor implements Extractor { private boolean ptsFlag; private int extendedHeaderLength; - private int payloadSize; - private long timeUs; public PesReader(ElementaryStreamReader pesPayloadReader) { @@ -322,6 +368,14 @@ public final class TsExtractor implements Extractor { state = STATE_FINDING_HEADER; } + @Override + public void seek() { + state = STATE_FINDING_HEADER; + bytesRead = 0; + bodyStarted = false; + pesPayloadReader.seek(); + } + @Override public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, ExtractorOutput output) { diff --git a/library/src/main/java/com/google/android/exoplayer/util/H264Util.java b/library/src/main/java/com/google/android/exoplayer/util/H264Util.java index 85691ae8f1..0cde8b472f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/H264Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/H264Util.java @@ -144,6 +144,17 @@ public final class H264Util { return endOffset; } + /** + * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}. + * + * @param prefixFlags The flags to clear. + */ + public static void clearPrefixFlags(boolean[] prefixFlags) { + prefixFlags[0] = false; + prefixFlags[1] = false; + prefixFlags[2] = false; + } + /** * Reads an unsigned integer into an integer. This method is suitable for use when it can be * assumed that the top bit will always be set to zero. @@ -162,17 +173,6 @@ public final class H264Util { return result; } - /** - * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}. - * - * @param prefixFlags The flags to clear. - */ - private static void clearPrefixFlags(boolean[] prefixFlags) { - prefixFlags[0] = false; - prefixFlags[1] = false; - prefixFlags[2] = false; - } - private H264Util() { // Prevent instantiation. }