diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12b5bcf219..816cce4233 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,9 +3,11 @@ We'd love to hear your feedback. Please open new issues describing any bugs, feature requests or suggestions that you have. -We are not actively looking to accept patches to this project at the current -time, however in some cases we may do so. For such cases, please see the -agreement below. +We will also consider high quality pull requests. These should normally merge +into the [dev][] branch rather than master. To contribute in this way you must +first submit a Contributor License Agreement, as described below. + +[dev]: https://github.com/google/ExoPlayer/tree/dev ## Contributor License Agreement ## diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 596cd5cacc..3a233ef204 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ 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 7817123830..a22e825960 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 @@ -117,9 +117,12 @@ import java.util.Locale; new Sample("Apple master playlist advanced", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/" + "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS), - new Sample("Apple single media playlist", + new Sample("Apple TS media playlist", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/" + "prog_index.m3u8", DemoUtil.TYPE_HLS), + new Sample("Apple AAC media playlist", + "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/" + + "prog_index.m3u8", DemoUtil.TYPE_HLS), }; public static final Sample[] MISC = new Sample[] { diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 67b902aff9..9edd786077 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.2.0"; + public static final String VERSION = "1.3.0"; /** * The version of the library, expressed as an integer. @@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 001002003. */ - public static final int VERSION_INT = 001002000; + public static final int VERSION_INT = 001003000; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index d8791f84b0..c51be31c5c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -417,7 +417,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { if (currentLoadable != null && mediaChunk == currentLoadable) { // Linearly interpolate partially-fetched chunk times. long chunkLength = mediaChunk.getLength(); - if (chunkLength != C.LENGTH_UNBOUNDED) { + if (chunkLength != C.LENGTH_UNBOUNDED && chunkLength != 0) { return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) * mediaChunk.bytesLoaded()) / chunkLength; } else { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 90bc497478..dc70320f0d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.hls.parser.AdtsExtractor; +import com.google.android.exoplayer.hls.parser.HlsExtractor; import com.google.android.exoplayer.hls.parser.TsExtractor; import com.google.android.exoplayer.upstream.Aes128DataSource; import com.google.android.exoplayer.upstream.BandwidthMeter; @@ -105,6 +107,7 @@ public class HlsChunkSource { public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000; private static final String TAG = "HlsChunkSource"; + private static final String AAC_FILE_EXTENSION = ".aac"; private static final float BANDWIDTH_FRACTION = 0.8f; private final BufferPool bufferPool; @@ -332,9 +335,11 @@ public class HlsChunkSource { boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1; // Configure the extractor that will read the chunk. - TsExtractor extractor; + HlsExtractor extractor; if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) { - extractor = new TsExtractor(startTimeUs, switchingVariantSpliced, bufferPool); + extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION) + ? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool) + : new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool); } else { extractor = previousTsChunk.extractor; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index a2497e3218..0205364718 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -60,7 +60,7 @@ public final class HlsPlaylistParser implements ManifestParser { private static final Pattern BANDWIDTH_ATTR_REGEX = Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b"); private static final Pattern CODECS_ATTR_REGEX = - Pattern.compile(CODECS_ATTR + "=\"(.+)\""); + Pattern.compile(CODECS_ATTR + "=\"(.+?)\""); private static final Pattern RESOLUTION_ATTR_REGEX = Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)"); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 4603577ff9..b8fffd4c11 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.hls.parser.TsExtractor; +import com.google.android.exoplayer.hls.parser.HlsExtractor; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; @@ -44,7 +44,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private static final int NO_RESET_PENDING = -1; private final HlsChunkSource chunkSource; - private final LinkedList extractors; + private final LinkedList extractors; private final boolean frameAccurateSeeking; private final int minLoadableRetryCount; @@ -83,7 +83,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; this.minLoadableRetryCount = minLoadableRetryCount; - extractors = new LinkedList(); + extractors = new LinkedList(); } @Override @@ -96,7 +96,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } continueBufferingInternal(); if (!extractors.isEmpty()) { - TsExtractor extractor = extractors.getFirst(); + HlsExtractor extractor = extractors.getFirst(); if (extractor.isPrepared()) { trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; @@ -195,7 +195,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return NOTHING_READ; } - TsExtractor extractor = getCurrentExtractor(); + HlsExtractor extractor = getCurrentExtractor(); if (extractors.size() > 1) { // If there's more than one extractor, attempt to configure a seamless splice from the // current one to the next one. @@ -328,8 +328,8 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { * * @return The current extractor from which samples should be read. Guaranteed to be non-null. */ - private TsExtractor getCurrentExtractor() { - TsExtractor extractor = extractors.getFirst(); + private HlsExtractor getCurrentExtractor() { + HlsExtractor extractor = extractors.getFirst(); while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { // We're finished reading from the extractor for all tracks, and so can discard it. extractors.removeFirst().release(); @@ -338,7 +338,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return extractor; } - private void discardSamplesForDisabledTracks(TsExtractor extractor, long timeUs) { + private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) { if (!extractor.isPrepared()) { return; } @@ -349,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } } - private boolean haveSamplesForEnabledTracks(TsExtractor extractor) { + private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) { if (!extractor.isPrepared()) { return false; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 36c1e30c8f..a66330bb5c 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.hls.parser.TsExtractor; +import com.google.android.exoplayer.hls.parser.HlsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; @@ -51,7 +51,7 @@ public final class TsChunk extends HlsChunk { /** * The extractor into which this chunk is being consumed. */ - public final TsExtractor extractor; + public final HlsExtractor extractor; private int loadPosition; private volatile boolean loadFinished; @@ -60,16 +60,17 @@ public final class TsChunk extends HlsChunk { /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. + * @param extractor An extractor to parse samples from the data. * @param variantIndex The index of the variant in the master playlist. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param chunkIndex The index of the chunk. * @param isLastChunk True if this is the last chunk in the media. False otherwise. */ - public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor, + public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractor extractor, int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { super(dataSource, dataSpec); - this.extractor = tsExtractor; + this.extractor = extractor; this.variantIndex = variantIndex; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java new file mode 100644 index 0000000000..af164a5f36 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 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.exoplayer.hls.parser; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.upstream.BufferPool; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS + * headers. + */ +public class AdtsExtractor extends HlsExtractor { + + private static final int MAX_PACKET_SIZE = 200; + + private final long firstSampleTimestamp; + private final ParsableByteArray packetBuffer; + private final AdtsReader adtsReader; + + // Accessed only by the loading thread. + private boolean firstPacket; + // Accessed by both the loading and consuming threads. + private volatile boolean prepared; + + public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) { + super(shouldSpliceIn); + this.firstSampleTimestamp = firstSampleTimestamp; + packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); + adtsReader = new AdtsReader(bufferPool); + firstPacket = true; + } + + @Override + public int getTrackCount() { + Assertions.checkState(prepared); + return 1; + } + + @Override + public MediaFormat getFormat(int track) { + Assertions.checkState(prepared); + return adtsReader.getMediaFormat(); + } + + @Override + public boolean isPrepared() { + return prepared; + } + + @Override + public void release() { + adtsReader.release(); + } + + @Override + public long getLargestSampleTimestamp() { + return adtsReader.getLargestParsedTimestampUs(); + } + + @Override + public boolean getSample(int track, SampleHolder holder) { + Assertions.checkState(prepared); + Assertions.checkState(track == 0); + return adtsReader.getSample(holder); + } + + @Override + public void discardUntil(int track, long timeUs) { + Assertions.checkState(prepared); + Assertions.checkState(track == 0); + adtsReader.discardUntil(timeUs); + } + + @Override + public boolean hasSamples(int track) { + Assertions.checkState(prepared); + Assertions.checkState(track == 0); + return !adtsReader.isEmpty(); + } + + @Override + public int read(DataSource dataSource) throws IOException { + int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE); + if (bytesRead == -1) { + return -1; + } + + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesRead); + + // TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket); + firstPacket = false; + if (!prepared) { + prepared = adtsReader.hasMediaFormat(); + } + return bytesRead; + } + + @Override + protected SampleQueue getSampleQueue(int track) { + Assertions.checkState(track == 0); + return adtsReader; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java index 35813052ad..9dec6cc84a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java @@ -30,7 +30,7 @@ import java.util.Collections; /** * Parses a continuous ADTS byte stream and extracts individual frames. */ -/* package */ class AdtsReader extends PesPayloadReader { +/* package */ class AdtsReader extends ElementaryStreamReader { private static final int STATE_FINDING_SYNC = 0; private static final int STATE_READING_HEADER = 1; @@ -137,6 +137,8 @@ import java.util.Collections; if (found) { hasCrc = (adtsData[i] & 0x1) == 0; pesBuffer.setPosition(i + 1); + // Reset lastByteWasFF for next time. + lastByteWasFF = false; return true; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java similarity index 87% rename from library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java rename to library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java index 2bdce8448a..a8c5c7b562 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/PesPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java @@ -19,11 +19,11 @@ import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.util.ParsableByteArray; /** - * Extracts individual samples from continuous byte stream, preserving original order. + * Extracts individual samples from an elementary media stream, preserving original order. */ -/* package */ abstract class PesPayloadReader extends SampleQueue { +/* package */ abstract class ElementaryStreamReader extends SampleQueue { - protected PesPayloadReader(BufferPool bufferPool) { + protected ElementaryStreamReader(BufferPool bufferPool) { super(bufferPool); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java index 55faeefcf4..7e94376c32 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java @@ -30,7 +30,7 @@ import java.util.List; /** * Parses a continuous H264 byte stream and extracts individual frames. */ -/* package */ class H264Reader extends PesPayloadReader { +/* package */ class H264Reader extends ElementaryStreamReader { private static final int NAL_UNIT_TYPE_IDR = 5; private static final int NAL_UNIT_TYPE_SEI = 6; @@ -44,6 +44,8 @@ import java.util.List; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer sei; + private int scratchEscapeCount; + private int[] scratchEscapePositions; private boolean isKeyframe; public H264Reader(BufferPool bufferPool, SeiReader seiReader) { @@ -53,6 +55,7 @@ import java.util.List; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); + scratchEscapePositions = new int[10]; } @Override @@ -133,7 +136,8 @@ import java.util.List; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (sei.endNalUnit(discardPadding)) { - seiReader.read(sei.nalData, 0, pesTimeUs); + int unescapedLength = unescapeStream(sei.nalData, sei.nalLength); + seiReader.read(sei.nalData, 0, unescapedLength, pesTimeUs); } } @@ -147,8 +151,8 @@ import java.util.List; initializationData.add(ppsData); // Unescape and then parse the SPS unit. - byte[] unescapedSps = unescapeStream(spsData, 0, spsData.length); - ParsableBitArray bitArray = new ParsableBitArray(unescapedSps); + unescapeStream(sps.nalData, sps.nalLength); + ParsableBitArray bitArray = new ParsableBitArray(sps.nalData); bitArray.skipBits(32); // NAL header int profileIdc = bitArray.readBits(8); bitArray.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) @@ -242,36 +246,45 @@ import java.util.List; } /** - * Replaces occurrences of [0, 0, 3] with [0, 0]. + * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with + * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length. *

* See ISO/IEC 14496-10:2005(E) page 36 for more information. + * + * @param data The data to unescape. + * @param limit The limit (exclusive) of the data to unescape. + * @return The length of the unescaped data. */ - private byte[] unescapeStream(byte[] data, int offset, int limit) { - int position = offset; - List escapePositions = new ArrayList(); + private int unescapeStream(byte[] data, int limit) { + int position = 0; + scratchEscapeCount = 0; while (position < limit) { position = findNextUnescapeIndex(data, position, limit); if (position < limit) { - escapePositions.add(position); + if (scratchEscapePositions.length <= scratchEscapeCount) { + // Grow scratchEscapePositions to hold a larger number of positions. + scratchEscapePositions = Arrays.copyOf(scratchEscapePositions, + scratchEscapePositions.length * 2); + } + scratchEscapePositions[scratchEscapeCount++] = position; position += 3; } } - int escapeCount = escapePositions.size(); - int escapedPosition = offset; // The position being read from. + int unescapedLength = limit - scratchEscapeCount; + int escapedPosition = 0; // The position being read from. int unescapedPosition = 0; // The position being written to. - byte[] unescapedData = new byte[limit - offset - escapeCount]; - for (int i = 0; i < escapeCount; i++) { - int nextEscapePosition = escapePositions.get(i); + for (int i = 0; i < scratchEscapeCount; i++) { + int nextEscapePosition = scratchEscapePositions[i]; int copyLength = nextEscapePosition - escapedPosition; - System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); + System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength); escapedPosition += copyLength + 3; unescapedPosition += copyLength + 2; } - int remainingLength = unescapedData.length - unescapedPosition; - System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); - return unescapedData; + int remainingLength = unescapedLength - unescapedPosition; + System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength); + return unescapedLength; } private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java new file mode 100644 index 0000000000..88aef4a0d6 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014 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.exoplayer.hls.parser; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.upstream.DataSource; + +import java.io.IOException; + +/** + * Facilitates extraction of media samples for HLS playbacks. + */ +// TODO: Consider consolidating more common logic in this base class. +public abstract class HlsExtractor { + + private final boolean shouldSpliceIn; + + // Accessed only by the consuming thread. + private boolean spliceConfigured; + + public HlsExtractor(boolean shouldSpliceIn) { + this.shouldSpliceIn = shouldSpliceIn; + } + + /** + * Attempts to configure a splice from this extractor to the next. + *

+ * The splice is performed such that for each track the samples read from the next extractor + * start with a keyframe, and continue from where the samples read from this extractor finish. + * A successful splice may discard samples from either or both extractors. + *

+ * Splice configuration may fail if the next extractor is not yet in a state that allows the + * splice to be performed. Calling this method is a noop if the splice has already been + * configured. Hence this method should be called repeatedly during the window within which a + * splice can be performed. + * + * @param nextExtractor The extractor being spliced to. + */ + public final void configureSpliceTo(HlsExtractor nextExtractor) { + if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { + // The splice is already configured, or the next extractor doesn't want to be spliced in, or + // the next extractor isn't ready to be spliced in. + return; + } + boolean spliceConfigured = true; + int trackCount = getTrackCount(); + for (int i = 0; i < trackCount; i++) { + spliceConfigured &= getSampleQueue(i).configureSpliceTo(nextExtractor.getSampleQueue(i)); + } + this.spliceConfigured = spliceConfigured; + return; + } + + /** + * Gets the number of available tracks. + *

+ * This method should only be called after the extractor has been prepared. + * + * @return The number of available tracks. + */ + public abstract int getTrackCount(); + + /** + * Gets the format of the specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track index. + * @return The corresponding format. + */ + public abstract MediaFormat getFormat(int track); + + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + public abstract boolean isPrepared(); + + /** + * Releases the extractor, recycling any pending or incomplete samples to the sample pool. + *

+ * This method should not be called whilst {@link #read(DataSource)} is also being invoked. + */ + public abstract void release(); + + /** + * Gets the largest timestamp of any sample parsed by the extractor. + * + * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. + */ + public abstract long getLargestSampleTimestamp(); + + /** + * Gets the next sample for the specified track. + * + * @param track The track from which to read. + * @param holder A {@link SampleHolder} into which the sample should be read. + * @return True if a sample was read. False otherwise. + */ + public abstract boolean getSample(int track, SampleHolder holder); + + /** + * Discards samples for the specified track up to the specified time. + * + * @param track The track from which samples should be discarded. + * @param timeUs The time up to which samples should be discarded, in microseconds. + */ + public abstract void discardUntil(int track, long timeUs); + + /** + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the + * specified track. + * + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} + * for the specified track. False otherwise. + */ + public abstract boolean hasSamples(int track); + + /** + * Reads up to a single TS packet. + * + * @param dataSource The {@link DataSource} from which to read. + * @throws IOException If an error occurred reading from the source. + * @return The number of bytes read from the source. + */ + public abstract int read(DataSource dataSource) throws IOException; + + /** + * Gets the {@link SampleQueue} for the specified track. + * + * @param track The track index. + * @return The corresponding sample queue. + */ + protected abstract SampleQueue getSampleQueue(int track); + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java index 609337b664..7de263d6da 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/Id3Reader.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; /** * Parses ID3 data and extracts individual text information frames. */ -/* package */ class Id3Reader extends PesPayloadReader { +/* package */ class Id3Reader extends ElementaryStreamReader { public Id3Reader(BufferPool bufferPool) { super(bufferPool); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java index 6da719ae22..ba7a91aa7d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/SeiReader.java @@ -36,14 +36,33 @@ import com.google.android.exoplayer.util.ParsableByteArray; seiBuffer = new ParsableByteArray(); } - public void read(byte[] data, int position, long pesTimeUs) { - seiBuffer.reset(data, data.length); + public void read(byte[] data, int position, int limit, long pesTimeUs) { + seiBuffer.reset(data, limit); + // Skip the NAL prefix and type. seiBuffer.setPosition(position + 4); - int ccDataSize = Eia608Parser.parseHeader(seiBuffer); - if (ccDataSize > 0) { - startSample(pesTimeUs); - appendData(seiBuffer, ccDataSize); - commitSample(true); + + int b; + while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { + // Parse payload type. + int payloadType = 0; + do { + b = seiBuffer.readUnsignedByte(); + payloadType += b; + } while (b == 0xFF); + // Parse payload size. + int payloadSize = 0; + do { + b = seiBuffer.readUnsignedByte(); + payloadSize += b; + } while (b == 0xFF); + // Process the payload. We only support EIA-608 payloads currently. + if (Eia608Parser.inspectSeiMessage(payloadType, payloadSize, seiBuffer)) { + startSample(pesTimeUs); + appendData(seiBuffer, payloadSize); + commitSample(true); + } else { + seiBuffer.skip(payloadSize); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java index d7ad5e7dde..8468254440 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java @@ -32,7 +32,7 @@ import java.io.IOException; /** * Facilitates the extraction of data from the MPEG-2 TS container format. */ -public final class TsExtractor { +public final class TsExtractor extends HlsExtractor { private static final String TAG = "TsExtractor"; @@ -51,13 +51,9 @@ public final class TsExtractor { private final SparseArray sampleQueues; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final BufferPool bufferPool; - private final boolean shouldSpliceIn; private final long firstSampleTimestamp; private final ParsableBitArray tsScratch; - // Accessed only by the consuming thread. - private boolean spliceConfigured; - // Accessed only by the loading thread. private int tsPacketBytesRead; private long timestampOffsetUs; @@ -66,9 +62,9 @@ public final class TsExtractor { // Accessed by both the loading and consuming threads. private volatile boolean prepared; - public TsExtractor(long firstSampleTimestamp, boolean shouldSpliceIn, BufferPool bufferPool) { + public TsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) { + super(shouldSpliceIn); this.firstSampleTimestamp = firstSampleTimestamp; - this.shouldSpliceIn = shouldSpliceIn; this.bufferPool = bufferPool; tsScratch = new ParsableBitArray(new byte[3]); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); @@ -78,86 +74,31 @@ public final class TsExtractor { lastPts = Long.MIN_VALUE; } - /** - * Gets the number of available tracks. - *

- * This method should only be called after the extractor has been prepared. - * - * @return The number of available tracks. - */ + @Override public int getTrackCount() { Assertions.checkState(prepared); return sampleQueues.size(); } - /** - * Gets the format of the specified track. - *

- * This method must only be called after the extractor has been prepared. - * - * @param track The track index. - * @return The corresponding format. - */ + @Override public MediaFormat getFormat(int track) { Assertions.checkState(prepared); return sampleQueues.valueAt(track).getMediaFormat(); } - /** - * Whether the extractor is prepared. - * - * @return True if the extractor is prepared. False otherwise. - */ + @Override public boolean isPrepared() { return prepared; } - /** - * Releases the extractor, recycling any pending or incomplete samples to the sample pool. - *

- * This method should not be called whilst {@link #read(DataSource)} is also being invoked. - */ + @Override public void release() { for (int i = 0; i < sampleQueues.size(); i++) { sampleQueues.valueAt(i).release(); } } - /** - * Attempts to configure a splice from this extractor to the next. - *

- * The splice is performed such that for each track the samples read from the next extractor - * start with a keyframe, and continue from where the samples read from this extractor finish. - * A successful splice may discard samples from either or both extractors. - *

- * Splice configuration may fail if the next extractor is not yet in a state that allows the - * splice to be performed. Calling this method is a noop if the splice has already been - * configured. Hence this method should be called repeatedly during the window within which a - * splice can be performed. - * - * @param nextExtractor The extractor being spliced to. - */ - public void configureSpliceTo(TsExtractor nextExtractor) { - Assertions.checkState(prepared); - if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { - // The splice is already configured, or the next extractor doesn't want to be spliced in, or - // the next extractor isn't ready to be spliced in. - return; - } - boolean spliceConfigured = true; - for (int i = 0; i < sampleQueues.size(); i++) { - spliceConfigured &= sampleQueues.valueAt(i).configureSpliceTo( - nextExtractor.sampleQueues.valueAt(i)); - } - this.spliceConfigured = spliceConfigured; - return; - } - - /** - * Gets the largest timestamp of any sample parsed by the extractor. - * - * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. - */ + @Override public long getLargestSampleTimestamp() { long largestParsedTimestampUs = Long.MIN_VALUE; for (int i = 0; i < sampleQueues.size(); i++) { @@ -167,36 +108,19 @@ public final class TsExtractor { return largestParsedTimestampUs; } - /** - * Gets the next sample for the specified track. - * - * @param track The track from which to read. - * @param holder A {@link SampleHolder} into which the sample should be read. - * @return True if a sample was read. False otherwise. - */ + @Override public boolean getSample(int track, SampleHolder holder) { Assertions.checkState(prepared); return sampleQueues.valueAt(track).getSample(holder); } - /** - * Discards samples for the specified track up to the specified time. - * - * @param track The track from which samples should be discarded. - * @param timeUs The time up to which samples should be discarded, in microseconds. - */ + @Override public void discardUntil(int track, long timeUs) { Assertions.checkState(prepared); sampleQueues.valueAt(track).discardUntil(timeUs); } - /** - * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the - * specified track. - * - * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} - * for the specified track. False otherwise. - */ + @Override public boolean hasSamples(int track) { Assertions.checkState(prepared); return !sampleQueues.valueAt(track).isEmpty(); @@ -215,13 +139,7 @@ public final class TsExtractor { return true; } - /** - * Reads up to a single TS packet. - * - * @param dataSource The {@link DataSource} from which to read. - * @throws IOException If an error occurred reading from the source. - * @return The number of bytes read from the source. - */ + @Override public int read(DataSource dataSource) throws IOException { int bytesRead = dataSource.read(tsPacketBuffer.data, tsPacketBytesRead, TS_PACKET_SIZE - tsPacketBytesRead); @@ -276,6 +194,11 @@ public final class TsExtractor { return bytesRead; } + @Override + protected SampleQueue getSampleQueue(int track) { + return sampleQueues.valueAt(track); + } + /** * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. * @@ -404,7 +327,7 @@ public final class TsExtractor { continue; } - PesPayloadReader pesPayloadReader = null; + ElementaryStreamReader pesPayloadReader = null; switch (streamType) { case TS_STREAM_TYPE_AAC: pesPayloadReader = new AdtsReader(bufferPool); @@ -444,7 +367,7 @@ public final class TsExtractor { private static final int MAX_HEADER_EXTENSION_SIZE = 5; private final ParsableBitArray pesScratch; - private final PesPayloadReader pesPayloadReader; + private final ElementaryStreamReader pesPayloadReader; private int state; private int bytesRead; @@ -457,7 +380,7 @@ public final class TsExtractor { private long timeUs; - public PesReader(PesPayloadReader pesPayloadReader) { + public PesReader(ElementaryStreamReader pesPayloadReader) { this.pesPayloadReader = pesPayloadReader; pesScratch = new ParsableBitArray(new byte[HEADER_SIZE]); state = STATE_FINDING_HEADER; diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index 7b744dfea6..e593858f8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -161,7 +161,7 @@ public final class Mp4Util { } } - int limit = endOffset - 2; + int limit = endOffset - 1; // We're looking for the NAL unit start code prefix 0x000001, followed by a byte that matches // the specified type. The value of i tracks the index of the third byte in the four bytes // being examined. diff --git a/library/src/main/java/com/google/android/exoplayer/source/DefaultSampleSource.java b/library/src/main/java/com/google/android/exoplayer/source/DefaultSampleSource.java index 918ff1c57a..ccad721abb 100644 --- a/library/src/main/java/com/google/android/exoplayer/source/DefaultSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/source/DefaultSampleSource.java @@ -88,7 +88,7 @@ public final class DefaultSampleSource implements SampleSource { Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); trackStates[track] = TRACK_STATE_ENABLED; sampleExtractor.selectTrack(track); - seekToUs(positionUs); + seekToUsInternal(positionUs, positionUs != 0); } @Override @@ -131,17 +131,7 @@ public final class DefaultSampleSource implements SampleSource { @Override public void seekToUs(long positionUs) { Assertions.checkState(prepared); - if (seekPositionUs != positionUs) { - // Avoid duplicate calls to the underlying extractor's seek method in the case that there - // have been no interleaving calls to readSample. - seekPositionUs = positionUs; - sampleExtractor.seekTo(positionUs); - for (int i = 0; i < trackStates.length; ++i) { - if (trackStates[i] != TRACK_STATE_DISABLED) { - pendingDiscontinuities[i] = true; - } - } - } + seekToUsInternal(positionUs, false); } @Override @@ -158,4 +148,18 @@ public final class DefaultSampleSource implements SampleSource { } } + private void seekToUsInternal(long positionUs, boolean force) { + // Unless forced, avoid duplicate calls to the underlying extractor's seek method in the case + // that there have been no interleaving calls to readSample. + if (force || seekPositionUs != positionUs) { + seekPositionUs = positionUs; + sampleExtractor.seekTo(positionUs); + for (int i = 0; i < trackStates.length; ++i) { + if (trackStates[i] != TRACK_STATE_DISABLED) { + pendingDiscontinuities[i] = true; + } + } + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java index a855e34839..dd34e504cc 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/eia608/Eia608Parser.java @@ -97,14 +97,17 @@ public class Eia608Parser { } /* package */ ClosedCaptionList parse(SampleHolder sampleHolder) { - if (sampleHolder.size <= 0) { + if (sampleHolder.size < 10) { return null; } captions.clear(); stringBuilder.setLength(0); seiBuffer.reset(sampleHolder.data.array()); - seiBuffer.skipBits(3); // reserved + process_cc_data_flag + zero_bit + + // country_code (8) + provider_code (16) + user_identifier (32) + user_data_type_code (8) + + // reserved (1) + process_cc_data_flag (1) + zero_bit (1) + seiBuffer.skipBits(67); int ccCount = seiBuffer.readBits(5); seiBuffer.skipBits(8); @@ -177,52 +180,28 @@ public class Eia608Parser { } /** - * Parses the beginning of SEI data and returns the size of underlying contains closed captions - * data following the header. Returns 0 if the SEI doesn't contain any closed captions data. + * Inspects an sei message to determine whether it contains EIA-608. + *

+ * The position of {@code payload} is left unchanged. * - * @param seiBuffer The buffer to read from. - * @return The size of closed captions data. + * @param payloadType The payload type of the message. + * @param payloadLength The length of the payload. + * @param payload A {@link ParsableByteArray} containing the payload. + * @return True if the sei message contains EIA-608. False otherwise. */ - public static int parseHeader(ParsableByteArray seiBuffer) { - int b = 0; - int payloadType = 0; - - do { - b = seiBuffer.readUnsignedByte(); - payloadType += b; - } while (b == 0xFF); - - if (payloadType != PAYLOAD_TYPE_CC) { - return 0; + public static boolean inspectSeiMessage(int payloadType, int payloadLength, + ParsableByteArray payload) { + if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { + return false; } - - int payloadSize = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadSize += b; - } while (b == 0xFF); - - if (payloadSize <= 0) { - return 0; - } - - int countryCode = seiBuffer.readUnsignedByte(); - if (countryCode != COUNTRY_CODE) { - return 0; - } - int providerCode = seiBuffer.readUnsignedShort(); - if (providerCode != PROVIDER_CODE) { - return 0; - } - int userIdentifier = seiBuffer.readInt(); - if (userIdentifier != USER_ID) { - return 0; - } - int userDataTypeCode = seiBuffer.readUnsignedByte(); - if (userDataTypeCode != USER_DATA_TYPE_CODE) { - return 0; - } - return payloadSize; + int startPosition = payload.getPosition(); + int countryCode = payload.readUnsignedByte(); + int providerCode = payload.readUnsignedShort(); + int userIdentifier = payload.readInt(); + int userDataTypeCode = payload.readUnsignedByte(); + payload.setPosition(startPosition); + return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE + && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; } }