From 3472e86c369e114cf8b9e1b03a55193816e11284 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 16 Feb 2015 22:22:25 +0000 Subject: [PATCH 01/11] Correctly reset ADTSreader state --- .../com/google/android/exoplayer/hls/parser/AdtsReader.java | 2 ++ 1 file changed, 2 insertions(+) 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..14fce5167d 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 @@ -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; } } From b46d1fc7cc2516cc29c629954ed0d69364098d97 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 16 Feb 2015 22:30:28 +0000 Subject: [PATCH 02/11] Bump dev version to 1.3.x --- demo/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer/ExoPlayerLibraryInfo.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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/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} From a1e196fe200629b096a9b85f3927cdd72a7428d7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 17 Feb 2015 15:41:59 +0000 Subject: [PATCH 03/11] Add support for elementary AAC/ADTS streams. - This change: 1. Extracts HlsExtractor interface from TsExtractor. 2. Adds AdtsExtractor for AAC/ADTS streams, which turned out to be really easy. Selection of the ADTS extractor relies on seeing the .aac extension. This is at least guaranteed not to break anything that works already (since no-one is going to be using .aac as the extension for something that's not elementary AAC/ADTS). Issue: #209 --- .../android/exoplayer/demo/Samples.java | 5 +- .../android/exoplayer/hls/HlsChunkSource.java | 9 +- .../exoplayer/hls/HlsSampleSource.java | 18 +-- .../google/android/exoplayer/hls/TsChunk.java | 9 +- .../exoplayer/hls/parser/AdtsExtractor.java | 126 +++++++++++++++ .../exoplayer/hls/parser/AdtsReader.java | 2 +- ...eader.java => ElementaryStreamReader.java} | 6 +- .../exoplayer/hls/parser/H264Reader.java | 2 +- .../exoplayer/hls/parser/HlsExtractor.java | 151 ++++++++++++++++++ .../exoplayer/hls/parser/Id3Reader.java | 2 +- .../exoplayer/hls/parser/TsExtractor.java | 118 +++----------- 11 files changed, 329 insertions(+), 119 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java rename library/src/main/java/com/google/android/exoplayer/hls/parser/{PesPayloadReader.java => ElementaryStreamReader.java} (87%) create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/parser/HlsExtractor.java 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/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/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 14fce5167d..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; 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..5390003a36 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; 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/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/parser/TsExtractor.java index d7ad5e7dde..b907042f71 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,12 @@ public final class TsExtractor { return bytesRead; } + @Override + protected SampleQueue getSampleQueue(int track) { + Assertions.checkState(track == 0); + return sampleQueues.valueAt(track); + } + /** * Adjusts a PTS value to the corresponding time in microseconds, accounting for PTS wraparound. * @@ -404,7 +328,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 +368,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 +381,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; From 526c64294a77a36ef543c6b48718794ab389c592 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 17 Feb 2015 16:04:44 +0000 Subject: [PATCH 04/11] Handle the edge case of zero-length chunks. Issue: #289 --- .../com/google/android/exoplayer/chunk/ChunkSampleSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From cc7a15b79b84673fddc2f711342b687002f66dc0 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 18 Feb 2015 15:09:45 +0000 Subject: [PATCH 05/11] Fix bad assertion --- .../com/google/android/exoplayer/hls/parser/TsExtractor.java | 1 - 1 file changed, 1 deletion(-) 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 b907042f71..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 @@ -196,7 +196,6 @@ public final class TsExtractor extends HlsExtractor { @Override protected SampleQueue getSampleQueue(int track) { - Assertions.checkState(track == 0); return sampleQueues.valueAt(track); } From b03c5c5753810a6d0076374f69aa8744059dd3e8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 18 Feb 2015 15:15:20 +0000 Subject: [PATCH 06/11] Ensure we always seek after selecting a track. Some extractor implementations underneath MediaExtractor require a seekTo call after tracks are selected to ensure samples are read from the correct position. De-duplicating logic was preventing this from happening in some cases, causing issues like: https://github.com/google/ExoPlayer/issues/301 Note that seeking all tracks a side effect of track selection sucks if you already have one or more tracks selected, because it introduces discontinuities to the already selected tracks. However, in general, it *is* necessary to specify the position for the track being selected, because the underlying extractor doesn't have enough information to know where to start reading from. It can't determine this based on the read positions of the already selected tracks, because the samples in these tracks might be very sparse with respect to time. I think a more optimal fix would be to change the SampleExtractor interface to receive the current position as an argument to selectTrack. For our own extractors, we'd seek the newly selected track to that position, whilst the already enabled tracks would be left in their current positions (if possible). For FrameworkSampleExtractor we'd still have no choice but to call seekTo on the extractor to seek all of the tracks. This solution ends up being more complex though, because: - The SampleExtractor then needs a way of telling DefaultSampleSource which tracks were actually seeked, so that the pendingDiscontinuities flags can be set correctly. - It's a weird API that requires the "current playback position to seek only the track being enabled" So it may not be worth it! I think this fix is definitely good for now, in any case. Issue: #301 --- .../exoplayer/source/DefaultSampleSource.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) 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; + } + } + } + } + } From abac6b7dd6662bb4893566a4a04bf9804ee0599d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 18 Feb 2015 19:11:52 +0000 Subject: [PATCH 07/11] Fix off-by-one-bug preventing NAL unit detection at the limit. --- .../src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 1649aa63813f66cabc4c7b293ba7868c9e68fbcf Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 18 Feb 2015 23:39:10 +0000 Subject: [PATCH 08/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12b5bcf219..7ee7db0c79 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, not 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 ## From f6a0cb963b68f7b36649ee7ead9c24d55b22f5f9 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 18 Feb 2015 23:42:42 +0000 Subject: [PATCH 09/11] Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ee7db0c79..816cce4233 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,8 @@ We'd love to hear your feedback. Please open new issues describing any bugs, feature requests or suggestions that you have. We will also consider high quality pull requests. These should normally merge -into the [dev][] branch, not master. To contribute in this way you must first -submit a Contributor License Agreement, as described below. +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 From e84bce61304dbfc639d2447deafbfcdcb37269b9 Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Thu, 19 Feb 2015 01:17:35 +0100 Subject: [PATCH 10/11] Fixed issue in CODEC regular expression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous regular expression for extracting codec information was wrong, given a line that defines a variant it added information from “CODEC=“ text to the end of the line (including also information about RESOLUTION or alternate rendition groups as part of the CODEC field). This is not causing a functional problem (at least known by me) although is making codecs field storing information that is not related with the codec. --- .../com/google/android/exoplayer/hls/HlsPlaylistParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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+)"); From b5100886896ad35a32e6105a9d005c3209a81a9b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 19 Feb 2015 11:22:42 +0000 Subject: [PATCH 11/11] Fix EIA-608 issues. - Data needs to be unescaped before it's passed to SeiReader. - SeiReader should loop over potentially multiple child messages. - I also changed the sample passed to the EIA-608 renderer so that it's the entire sei message payload. The first 8 bytes are unnecessary, but it seems nicer conceptually to do it this way. Issue: #295 --- .../exoplayer/hls/parser/H264Reader.java | 47 ++++++++----- .../exoplayer/hls/parser/SeiReader.java | 33 +++++++-- .../exoplayer/text/eia608/Eia608Parser.java | 69 +++++++------------ 3 files changed, 80 insertions(+), 69 deletions(-) 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 5390003a36..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 @@ -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/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/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; } }