diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java index 778a026228..9451add3cb 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/HlsRendererBuilder.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer.demo.full.player; -import com.google.android.exoplayer.DefaultLoadControl; -import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; @@ -25,13 +23,13 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilderCallback; import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsMasterPlaylist; -import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; import com.google.android.exoplayer.hls.HlsMasterPlaylistParser; import com.google.android.exoplayer.hls.HlsSampleSource; +import com.google.android.exoplayer.hls.Variant; import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; -import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -47,9 +45,6 @@ import java.util.Collections; */ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int VIDEO_BUFFER_SEGMENTS = 200; - private final String userAgent; private final String url; private final String contentId; @@ -89,12 +84,12 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { - private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; - private static final int VIDEO_BUFFER_SEGMENTS = 200; - private final SimplePlayerActivity playerActivity; private final String userAgent; private final String url; @@ -90,12 +85,11 @@ import java.util.Collections; @Override public void onManifest(String contentId, HlsMasterPlaylist manifest) { - LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); - - DataSource dataSource = new UriDataSource(userAgent, null); - HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest); - HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, - VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 2); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter); + HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null, + false); + HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(), playerActivity, 50); @@ -109,7 +103,7 @@ import java.util.Collections; private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { return new HlsMasterPlaylist(Uri.parse(""), - Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1))); + Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1))); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java new file mode 100644 index 0000000000..cc56691441 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/BitArrayChunk.java @@ -0,0 +1,100 @@ +/* + * 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; + +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; +import com.google.android.exoplayer.util.BitArray; + +import java.io.IOException; + +/** + * An abstract base class for {@link HlsChunk} implementations where the data should be loaded into + * a {@link BitArray} and subsequently consumed. + */ +public abstract class BitArrayChunk extends HlsChunk { + + private static final int READ_GRANULARITY = 16 * 1024; + + private final BitArray bitArray; + private volatile boolean loadFinished; + private volatile boolean loadCanceled; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then + * the length resolved by {@code dataSource.open(dataSpec)} must not exceed + * {@link Integer#MAX_VALUE}. + * @param bitArray The {@link BitArray} into which the data should be loaded. + */ + public BitArrayChunk(DataSource dataSource, DataSpec dataSpec, BitArray bitArray) { + super(dataSource, dataSpec); + this.bitArray = bitArray; + } + + @Override + public void consume() throws IOException { + consume(bitArray); + } + + /** + * Invoked by {@link #consume()}. Implementations should override this method to consume the + * loaded data. + * + * @param bitArray The {@link BitArray} containing the loaded data. + * @throws IOException If an error occurs consuming the loaded data. + */ + protected abstract void consume(BitArray bitArray) throws IOException; + + /** + * Whether the whole of the chunk has been loaded. + * + * @return True if the whole of the chunk has been loaded. False otherwise. + */ + @Override + public boolean isLoadFinished() { + return loadFinished; + } + + // Loadable implementation + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + try { + bitArray.reset(); + dataSource.open(dataSpec); + int bytesRead = 0; + while (bytesRead != -1 && !loadCanceled) { + bytesRead = bitArray.append(dataSource, READ_GRANULARITY); + } + loadFinished = !loadCanceled; + } finally { + dataSource.close(); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java index 256921f9ea..4fe1a18646 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java @@ -15,38 +15,21 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.Allocation; -import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DataSourceStream; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.Loader.Loadable; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; import java.io.IOException; /** * An abstract base class for {@link Loadable} implementations that load chunks of data required - * for the playback of streams. - *

- * TODO: Figure out whether this should merge with the chunk package, or whether the hls - * implementation is going to naturally diverge. + * for the playback of HLS streams. */ public abstract class HlsChunk implements Loadable { - /** - * The reason for a {@link HlsChunkSource} having generated this chunk. For reporting only. - * Possible values for this variable are defined by the specific {@link HlsChunkSource} - * implementations. - */ - public final int trigger; - - private final DataSource dataSource; - private final DataSpec dataSpec; - - private DataSourceStream dataSourceStream; + protected final DataSource dataSource; + protected final DataSpec dataSpec; /** * @param dataSource The source from which the data should be loaded. @@ -54,123 +37,15 @@ public abstract class HlsChunk implements Loadable { * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * the length resolved by {@code dataSource.open(dataSpec)} must not exceed * {@link Integer#MAX_VALUE}. - * @param trigger See {@link #trigger}. */ - public HlsChunk(DataSource dataSource, DataSpec dataSpec, int trigger) { + public HlsChunk(DataSource dataSource, DataSpec dataSpec) { Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE); this.dataSource = Assertions.checkNotNull(dataSource); this.dataSpec = Assertions.checkNotNull(dataSpec); - this.trigger = trigger; } - /** - * Initializes the {@link HlsChunk}. - * - * @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the - * data can be obtained. - */ - public final void init(Allocator allocator) { - Assertions.checkState(dataSourceStream == null); - dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator); - } + public abstract void consume() throws IOException; - /** - * Releases the {@link HlsChunk}, releasing any backing {@link Allocation}s. - */ - public final void release() { - if (dataSourceStream != null) { - dataSourceStream.close(); - dataSourceStream = null; - } - } - - /** - * Gets the length of the chunk in bytes. - * - * @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet - * to be determined. - */ - public final long getLength() { - return dataSourceStream.getLength(); - } - - /** - * Whether the whole of the data has been consumed. - * - * @return True if the whole of the data has been consumed. False otherwise. - */ - public final boolean isReadFinished() { - return dataSourceStream.isEndOfStream(); - } - - /** - * Whether the whole of the chunk has been loaded. - * - * @return True if the whole of the chunk has been loaded. False otherwise. - */ - public final boolean isLoadFinished() { - return dataSourceStream.isLoadFinished(); - } - - /** - * Gets the number of bytes that have been loaded. - * - * @return The number of bytes that have been loaded. - */ - public final long bytesLoaded() { - return dataSourceStream.getLoadPosition(); - } - - /** - * Causes loaded data to be consumed. - * - * @throws IOException If an error occurs consuming the loaded data. - */ - public final void consume() throws IOException { - Assertions.checkState(dataSourceStream != null); - consumeStream(dataSourceStream); - } - - /** - * Invoked by {@link #consume()}. Implementations may override this method if they wish to - * consume the loaded data at this point. - *

- * The default implementation is a no-op. - * - * @param stream The stream of loaded data. - * @throws IOException If an error occurs consuming the loaded data. - */ - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - // Do nothing. - } - - protected final NonBlockingInputStream getNonBlockingInputStream() { - return dataSourceStream; - } - - protected final void resetReadPosition() { - if (dataSourceStream != null) { - dataSourceStream.resetReadPosition(); - } else { - // We haven't been initialized yet, so the read position must already be 0. - } - } - - // Loadable implementation - - @Override - public final void cancelLoad() { - dataSourceStream.cancelLoad(); - } - - @Override - public final boolean isLoadCanceled() { - return dataSourceStream.isLoadCanceled(); - } - - @Override - public final void load() throws IOException, InterruptedException { - dataSourceStream.load(); - } + public abstract boolean isLoadFinished(); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java deleted file mode 100644 index 27b11c2ebd..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkOperationHolder.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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; - -/** - * Holds a hls chunk operation, which consists of a {@link HlsChunk} to load together with the - * number of {@link TsChunk}s that should be retained on the queue. - *

- * TODO: Figure out whether this should merge with the chunk package, or whether the hls - * implementation is going to naturally diverge. - */ -public final class HlsChunkOperationHolder { - - /** - * The number of {@link TsChunk}s to retain in a queue. - */ - public int queueSize; - - /** - * The chunk. - */ - public HlsChunk chunk; - -} 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 2dc1158cd5..06cf2a68d2 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 @@ -18,10 +18,12 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.hls.TsExtractor.SamplePool; import com.google.android.exoplayer.upstream.Aes128DataSource; +import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -30,6 +32,8 @@ import android.os.SystemClock; import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; @@ -41,26 +45,66 @@ import java.util.Locale; */ public class HlsChunkSource { + private static final float BANDWIDTH_FRACTION = 0.8f; + private static final long MIN_BUFFER_TO_SWITCH_UP_US = 5000000; + private static final long MAX_BUFFER_TO_SWITCH_DOWN_US = 15000000; + + private final SamplePool samplePool = new TsExtractor.SamplePool(); private final DataSource upstreamDataSource; - private final HlsMasterPlaylist masterPlaylist; private final HlsMediaPlaylistParser mediaPlaylistParser; + private final Variant[] enabledVariants; + private final BandwidthMeter bandwidthMeter; + private final BitArray bitArray; + private final boolean enableAdaptive; + private final Uri baseUri; + private final int maxWidth; + private final int maxHeight; - /* package */ HlsMediaPlaylist mediaPlaylist; - /* package */ boolean mediaPlaylistWasLive; - /* package */ long lastMediaPlaylistLoadTimeMs; + /* package */ final HlsMediaPlaylist[] mediaPlaylists; + /* package */ final long[] lastMediaPlaylistLoadTimesMs; + /* package */ boolean live; + /* package */ long durationUs; + private int variantIndex; private DataSource encryptedDataSource; private String encryptionKeyUri; - // TODO: Once proper m3u8 parsing is in place, actually use the url! - public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { + /** + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param masterPlaylist The master playlist. + * @param variantIndices A subset of variant indices to consider, or null to consider all of the + * variants in the master playlist. + */ + public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist, + BandwidthMeter bandwidthMeter, int[] variantIndices, boolean enableAdaptive) { this.upstreamDataSource = dataSource; - this.masterPlaylist = masterPlaylist; + this.bandwidthMeter = bandwidthMeter; + this.enableAdaptive = enableAdaptive; + baseUri = masterPlaylist.baseUri; + bitArray = new BitArray(); mediaPlaylistParser = new HlsMediaPlaylistParser(); + enabledVariants = filterVariants(masterPlaylist, variantIndices); + lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; + mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; + int maxWidth = -1; + int maxHeight = -1; + // Select the first variant from the master playlist that's enabled. + long minOriginalVariantIndex = Integer.MAX_VALUE; + for (int i = 0; i < enabledVariants.length; i++) { + if (enabledVariants[i].index < minOriginalVariantIndex) { + minOriginalVariantIndex = enabledVariants[i].index; + variantIndex = i; + } + maxWidth = Math.max(enabledVariants[i].width, maxWidth); + maxHeight = Math.max(enabledVariants[i].width, maxHeight); + } + // TODO: We should allow the default values to be passed through the constructor. + this.maxWidth = maxWidth > 0 ? maxWidth : 1920; + this.maxHeight = maxHeight > 0 ? maxHeight : 1080; } public long getDurationUs() { - return mediaPlaylistWasLive ? TrackRenderer.UNKNOWN_TIME_US : mediaPlaylist.durationUs; + return live ? TrackRenderer.UNKNOWN_TIME_US : durationUs; } /** @@ -72,49 +116,33 @@ public class HlsChunkSource { * @param out The {@link MediaFormat} on which the maximum video dimensions should be set. */ public void getMaxVideoDimensions(MediaFormat out) { - // TODO: Implement this. + out.setMaxVideoDimensions(maxWidth, maxHeight); } /** - * Updates the provided {@link HlsChunkOperationHolder} to contain the next operation that should - * be performed by the calling {@link HlsSampleSource}. - *

- * The next operation comprises of a possibly shortened queue length (shortened if the - * implementation wishes for the caller to discard {@link TsChunk}s from the queue), together - * with the next {@link HlsChunk} to load. The next chunk may be a {@link TsChunk} to be added to - * the queue, or another {@link HlsChunk} type (e.g. to load initialization data), or null if the - * source is not able to provide a chunk in its current state. + * Returns the next {@link HlsChunk} that should be loaded. * - * @param queue A representation of the currently buffered {@link TsChunk}s. - * @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If - * the queue is non-empty then this parameter is ignored. + * @param previousTsChunk The previously loaded chunk that the next chunk should follow. + * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek + * position. If there is a previous chunk then this parameter is ignored. * @param playbackPositionUs The current playback position. - * @param out A holder for the next operation, whose {@link HlsChunkOperationHolder#queueSize} is - * initially equal to the length of the queue, and whose {@linkHls ChunkOperationHolder#chunk} - * is initially equal to null or a {@link TsChunk} previously supplied by the - * {@link HlsChunkSource} that the caller has not yet finished loading. In the latter case the - * chunk can either be replaced or left unchanged. Note that leaving the chunk unchanged is - * both preferred and more efficient than replacing it with a new but identical chunk. + * @return The next chunk to load. */ - public void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, - HlsChunkOperationHolder out) { - if (out.chunk != null) { - // We already have a chunk. Keep it. - return; - } + public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, + long playbackPositionUs) { + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; if (mediaPlaylist == null) { - out.chunk = newMediaPlaylistChunk(); - return; + return newMediaPlaylistChunk(); } int chunkMediaSequence = 0; - if (mediaPlaylistWasLive) { - if (queue.isEmpty()) { + if (live) { + if (previousTsChunk == null) { chunkMediaSequence = getLiveStartChunkMediaSequence(); } else { // For live nextChunkIndex contains chunk media sequence number. - chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; + chunkMediaSequence = previousTsChunk.nextChunkIndex; // If the updated playlist is far ahead and doesn't even have the last chunk from the // queue, then try to catch up, skip a few chunks and start as if it was a new playlist. if (chunkMediaSequence < mediaPlaylist.mediaSequence) { @@ -124,28 +152,26 @@ public class HlsChunkSource { } } else { // Not live. - if (queue.isEmpty()) { + if (previousTsChunk == null) { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; } else { - chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; + chunkMediaSequence = previousTsChunk.nextChunkIndex; } } if (chunkMediaSequence == -1) { - out.chunk = null; - return; + // We've reached the end of the stream. + return null; } int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; - // If the end of the playlist is reached. if (chunkIndex >= mediaPlaylist.segments.size()) { if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) { - out.chunk = newMediaPlaylistChunk(); + return newMediaPlaylistChunk(); } else { - out.chunk = null; + return null; } - return; } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); @@ -156,97 +182,204 @@ public class HlsChunkSource { if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); - out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV); + HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV); encryptionKeyUri = segment.encryptionKeyUri; - return; + return toReturn; } } else { encryptedDataSource = null; encryptionKeyUri = null; } - DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); - long startTimeUs; + boolean splicingIn = previousTsChunk != null && previousTsChunk.splicingOut; int nextChunkMediaSequence = chunkMediaSequence + 1; - if (mediaPlaylistWasLive) { - if (queue.isEmpty()) { + if (live) { + if (previousTsChunk == null) { startTimeUs = 0; + } else if (splicingIn) { + startTimeUs = previousTsChunk.startTimeUs; } else { - startTimeUs = queue.get(queue.size() - 1).endTimeUs; + startTimeUs = previousTsChunk.endTimeUs; } } else { // Not live. startTimeUs = segment.startTimeUs; - if (chunkIndex == mediaPlaylist.segments.size() - 1) { - nextChunkMediaSequence = -1; - } } + if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) { + nextChunkMediaSequence = -1; + } + long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); + int currentVariantIndex = variantIndex; + boolean splicingOut = false; + if (splicingIn) { + // Do nothing. + } else if (enableAdaptive && nextChunkMediaSequence != -1) { + int idealVariantIndex = getVariantIndexForBandwdith( + (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); + long bufferedUs = startTimeUs - playbackPositionUs; + if ((idealVariantIndex > currentVariantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US) + || (idealVariantIndex < currentVariantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) { + variantIndex = idealVariantIndex; + } + splicingOut = variantIndex != currentVariantIndex; + if (splicingOut) { + // If we're splicing out, we want to load the same chunk again next time, but for a + // different variant. + nextChunkMediaSequence = chunkMediaSequence; + } + } + + // Configure the datasource for loading the chunk. DataSource dataSource; if (encryptedDataSource != null) { dataSource = encryptedDataSource; } else { dataSource = upstreamDataSource; } - out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs, - nextChunkMediaSequence, segment.discontinuity, false); + DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null); + + // Configure the extractor that will read the chunk. + TsExtractor extractor; + if (previousTsChunk == null || splicingIn || segment.discontinuity) { + extractor = new TsExtractor(startTimeUs, samplePool); + } else { + extractor = previousTsChunk.extractor; + } + if (splicingOut) { + extractor.discardFromNextKeyframes(); + } + + return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[currentVariantIndex].index, + startTimeUs, endTimeUs, nextChunkMediaSequence, splicingOut); + } + + private int getVariantIndexForBandwdith(int bandwidth) { + for (int i = 0; i < enabledVariants.length - 1; i++) { + if (enabledVariants[i].bandwidth <= bandwidth) { + return i; + } + } + return enabledVariants.length - 1; } private boolean shouldRerequestMediaPlaylist() { // Don't re-request media playlist more often than one-half of the target duration. + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; long timeSinceLastMediaPlaylistLoadMs = - SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimeMs; + SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex]; return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; } private int getLiveStartChunkMediaSequence() { // For live start playback from the third chunk from the end. + HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex]; int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; return chunkIndex + mediaPlaylist.mediaSequence; } private MediaPlaylistChunk newMediaPlaylistChunk() { - Uri mediaPlaylistUri = Util.getMergedUri(masterPlaylist.baseUri, - masterPlaylist.variants.get(0).url); + Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); - Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); - return new MediaPlaylistChunk(upstreamDataSource, dataSpec, 0, mediaPlaylistBaseUri); + Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); + return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null); - return new EncryptionKeyChunk(upstreamDataSource, dataSpec, 0, iv); + return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv); } - private class MediaPlaylistChunk extends HlsChunk { + private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) { + List masterVariants = masterPlaylist.variants; + ArrayList enabledVariants = new ArrayList(); + if (variantIndices != null) { + for (int i = 0; i < variantIndices.length; i++) { + enabledVariants.add(masterVariants.get(variantIndices[i])); + } + } else { + // If variantIndices is null then all variants are initially considered. + enabledVariants.addAll(masterVariants); + } - private final Uri baseUri; + ArrayList definiteVideoVariants = new ArrayList(); + ArrayList definiteAudioOnlyVariants = new ArrayList(); + for (int i = 0; i < enabledVariants.size(); i++) { + Variant variant = enabledVariants.get(i); + if (variant.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { + definiteVideoVariants.add(variant); + } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { + definiteAudioOnlyVariants.add(variant); + } + } - public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Uri baseUri) { - super(dataSource, dataSpec, trigger); - this.baseUri = baseUri; + if (!definiteVideoVariants.isEmpty()) { + // We've identified some variants as definitely containing video. Assume variants within the + // master playlist are marked consistently, and hence that we have the full set. Filter out + // any other variants, which are likely to be audio only. + enabledVariants = definiteVideoVariants; + } else if (definiteAudioOnlyVariants.size() < enabledVariants.size()) { + // We've identified some variants, but not all, as being audio only. Filter them out to leave + // the remaining variants, which are likely to contain video. + enabledVariants.removeAll(definiteAudioOnlyVariants); + } else { + // Leave the enabled variants unchanged. They're likely either all video or all audio. + } + + Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator()); + + Variant[] enabledVariantsArray = new Variant[enabledVariants.size()]; + enabledVariants.toArray(enabledVariantsArray); + return enabledVariantsArray; + } + + private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { + String[] codecs = variant.codecs; + if (codecs == null) { + return false; + } + for (int i = 0; i < codecs.length; i++) { + if (codecs[i].startsWith(prefix)) { + return true; + } + } + return false; + } + + private class MediaPlaylistChunk extends BitArrayChunk { + + @SuppressWarnings("hiding") + private final int variantIndex; + private final Uri playlistBaseUri; + + public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, + Uri playlistBaseUri) { + super(dataSource, dataSpec, bitArray); + this.variantIndex = variantIndex; + this.playlistBaseUri = playlistBaseUri; } @Override - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - byte[] data = new byte[(int) stream.getAvailableByteCount()]; - stream.read(data, 0, data.length); - lastMediaPlaylistLoadTimeMs = SystemClock.elapsedRealtime(); - mediaPlaylist = mediaPlaylistParser.parse( - new ByteArrayInputStream(data), null, null, baseUri); - mediaPlaylistWasLive |= mediaPlaylist.live; + protected void consume(BitArray data) throws IOException { + HlsMediaPlaylist mediaPlaylist = mediaPlaylistParser.parse( + new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null, + playlistBaseUri); + mediaPlaylists[variantIndex] = mediaPlaylist; + lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); + live |= mediaPlaylist.live; + durationUs = mediaPlaylist.durationUs; } } - private class EncryptionKeyChunk extends HlsChunk { + private class EncryptionKeyChunk extends BitArrayChunk { private final String iv; - public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, int trigger, String iv) { - super(dataSource, dataSpec, trigger); + public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { + super(dataSource, dataSpec, bitArray); if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { this.iv = iv.substring(2); } else { @@ -255,9 +388,9 @@ public class HlsChunkSource { } @Override - protected void consumeStream(NonBlockingInputStream stream) throws IOException { - byte[] keyData = new byte[(int) stream.getAvailableByteCount()]; - stream.read(keyData, 0, keyData.length); + protected void consume(BitArray data) throws IOException { + byte[] secretKey = new byte[data.bytesLeft()]; + data.readBytes(secretKey, 0, secretKey.length); int ivParsed = Integer.parseInt(iv, 16); String iv = String.format("%032X", ivParsed); @@ -267,7 +400,7 @@ public class HlsChunkSource { System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length, ivData.length); - encryptedDataSource = new Aes128DataSource(keyData, ivDataWithPadding, upstreamDataSource); + encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource); } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java index 8d7ff1bc61..ade40ebb3a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -24,25 +24,6 @@ import java.util.List; */ public final class HlsMasterPlaylist { - /** - * Variant stream reference. - */ - public static final class Variant { - public final int bandwidth; - public final String url; - public final String[] codecs; - public final int width; - public final int height; - - public Variant(String url, int bandwidth, String[] codecs, int width, int height) { - this.bandwidth = bandwidth; - this.url = url; - this.codecs = codecs; - this.width = width; - this.height = height; - } - } - public final Uri baseUri; public final List variants; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java index d0cb58baa9..3b2ed82b7c 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParser.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant; import com.google.android.exoplayer.util.ManifestParser; import android.net.Uri; @@ -61,6 +60,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser - * TODO: Figure out whether this should merge with the chunk package, or whether the hls - * implementation is going to naturally diverge. */ public class HlsSampleSource implements SampleSource, Loader.Callback { - private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000; + private static final long BUFFER_DURATION_US = 20000000; private static final int NO_RESET_PENDING = -1; - private final TsExtractor.SamplePool samplePool; - private final LoadControl loadControl; private final HlsChunkSource chunkSource; - private final HlsChunkOperationHolder currentLoadableHolder; private final LinkedList extractors; - private final LinkedList mediaChunks; - private final List readOnlyHlsChunks; - private final int bufferSizeContribution; private final boolean frameAccurateSeeking; private int remainingReleaseCount; @@ -70,7 +54,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; - private long lastPerformedBufferOperation; + + private TsChunk previousTsLoadable; + private HlsChunk currentLoadable; + private boolean loadingFinished; private Loader loader; private IOException currentLoadableException; @@ -78,18 +65,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; - public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, - int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { + public HlsSampleSource(HlsChunkSource chunkSource, + boolean frameAccurateSeeking, int downstreamRendererCount) { this.chunkSource = chunkSource; - this.loadControl = loadControl; - this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; - samplePool = new TsExtractor.SamplePool(); extractors = new LinkedList(); - currentLoadableHolder = new HlsChunkOperationHolder(); - mediaChunks = new LinkedList(); - readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks); } @Override @@ -99,13 +80,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } if (loader == null) { loader = new Loader("Loader:HLS"); - loadControl.register(this, bufferSizeContribution); } continueBufferingInternal(); if (extractors.isEmpty()) { return false; } - TsExtractor extractor = extractors.get(0); + TsExtractor extractor = extractors.getFirst(); if (extractor.isPrepared()) { trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; @@ -156,8 +136,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (loader.isLoading()) { loader.cancelLoading(); } else { - clearHlsChunks(); + discardExtractors(); clearCurrentLoadable(); + previousTsLoadable = null; } } } @@ -171,50 +152,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private boolean continueBufferingInternal() throws IOException { - updateLoadControl(); - if (isPendingReset()) { + maybeStartLoading(); + if (isPendingReset() || extractors.isEmpty()) { return false; } - - TsChunk mediaChunk = mediaChunks.getFirst(); - int currentVariant = mediaChunk.variantIndex; - - TsExtractor extractor; - if (extractors.isEmpty()) { - extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool); - extractors.addLast(extractor); - if (mediaChunk.discardFromFirstKeyframes) { - extractor.discardFromNextKeyframes(); - } - } else { - extractor = extractors.getLast(); - } - - if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) { - discardDownstreamHlsChunk(); - mediaChunk = mediaChunks.getFirst(); - if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) { - extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool); - extractors.addLast(extractor); - } - if (mediaChunk.discardFromFirstKeyframes) { - extractor.discardFromNextKeyframes(); - } - } - - // Allow the extractor to consume from the current chunk. - NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream(); - boolean haveSufficientSamples = extractor.consumeUntil(inputStream, - downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US); - if (!haveSufficientSamples) { - // If we can't read any more, then we always say we have sufficient samples. - haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished(); - } - - if (!haveSufficientSamples && currentLoadableException != null) { + boolean haveSamples = extractors.getFirst().hasSamples(); + if (!haveSamples && currentLoadableException != null) { throw currentLoadableException; } - return haveSufficientSamples; + return haveSamples; } @Override @@ -228,11 +174,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return DISCONTINUITY_READ; } - if (onlyReadDiscontinuity || isPendingReset()) { - return NOTHING_READ; - } - - if (extractors.isEmpty()) { + if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) { return NOTHING_READ; } @@ -266,12 +208,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return SAMPLE_READ; } - TsChunk mediaChunk = mediaChunks.getFirst(); - if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) { - return END_OF_STREAM; - } - - return NOTHING_READ; + return loadingFinished ? END_OF_STREAM : NOTHING_READ; } @Override @@ -283,32 +220,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (pendingResetPositionUs == positionUs) { return; } - for (int i = 0; i < pendingDiscontinuities.length; i++) { pendingDiscontinuities[i] = true; } - TsChunk mediaChunk = getHlsChunk(positionUs); - if (mediaChunk == null) { - restartFrom(positionUs); - } else { - discardExtractors(); - discardDownstreamHlsChunks(mediaChunk); - mediaChunk.resetReadPosition(); - updateLoadControl(); - } - } - - private TsChunk getHlsChunk(long positionUs) { - Iterator mediaChunkIterator = mediaChunks.iterator(); - while (mediaChunkIterator.hasNext()) { - TsChunk mediaChunk = mediaChunkIterator.next(); - if (positionUs < mediaChunk.startTimeUs) { - return null; - } else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) { - return mediaChunk; - } - } - return null; + restartFrom(positionUs); } @Override @@ -317,22 +232,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { Assertions.checkState(enabledTrackCount > 0); if (isPendingReset()) { return pendingResetPositionUs; - } - TsChunk mediaChunk = mediaChunks.getLast(); - HlsChunk currentLoadable = currentLoadableHolder.chunk; - if (currentLoadable != null && mediaChunk == currentLoadable) { - // Linearly interpolate partially-fetched chunk times. - long chunkLength = mediaChunk.getLength(); - if (chunkLength != C.LENGTH_UNBOUNDED) { - return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) - * mediaChunk.bytesLoaded()) / chunkLength; - } else { - return mediaChunk.startTimeUs; - } - } else if (mediaChunk.isLastChunk()) { + } else if (loadingFinished) { return TrackRenderer.END_OF_TRACK_US; } else { - return mediaChunk.endTimeUs; + return extractors.getLast().getLargestSampleTimestamp(); } } @@ -340,7 +243,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { public void release() { Assertions.checkState(remainingReleaseCount > 0); if (--remainingReleaseCount == 0 && loader != null) { - loadControl.unregister(this); loader.release(); loader = null; } @@ -348,7 +250,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void onLoadCompleted(Loadable loadable) { - HlsChunk currentLoadable = currentLoadableHolder.chunk; try { currentLoadable.consume(); } catch (IOException e) { @@ -357,28 +258,24 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionFatal = true; } finally { - if (!isTsChunk(currentLoadable)) { - currentLoadable.release(); + if (isTsChunk(currentLoadable)) { + TsChunk tsChunk = (TsChunk) loadable; + loadingFinished = tsChunk.isLastChunk(); } if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } - updateLoadControl(); + maybeStartLoading(); } } @Override public void onLoadCanceled(Loadable loadable) { - HlsChunk currentLoadable = currentLoadableHolder.chunk; - if (!isTsChunk(currentLoadable)) { - currentLoadable.release(); - } clearCurrentLoadable(); if (enabledTrackCount > 0) { restartFrom(pendingResetPositionUs); } else { - clearHlsChunks(); - loadControl.trimAllocator(); + previousTsLoadable = null; } } @@ -387,142 +284,65 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); - updateLoadControl(); + maybeStartLoading(); } private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; + previousTsLoadable = null; + loadingFinished = false; + discardExtractors(); if (loader.isLoading()) { loader.cancelLoading(); } else { - clearHlsChunks(); - clearCurrentLoadable(); - updateLoadControl(); - } - } - - private void clearHlsChunks() { - discardDownstreamHlsChunks(null); - } - - private void clearCurrentLoadable() { - currentLoadableHolder.chunk = null; - currentLoadableException = null; - currentLoadableExceptionCount = 0; - currentLoadableExceptionFatal = false; - } - - private void updateLoadControl() { - if (currentLoadableExceptionFatal) { - // We've failed, but we still need to update the control with our current state. - loadControl.update(this, downstreamPositionUs, -1, false, true); - return; - } - - long loadPositionUs; - if (isPendingReset()) { - loadPositionUs = pendingResetPositionUs; - } else { - TsChunk lastHlsChunk = mediaChunks.getLast(); - loadPositionUs = lastHlsChunk.nextChunkIndex == -1 ? -1 : lastHlsChunk.endTimeUs; - } - - boolean isBackedOff = currentLoadableException != null; - boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, - isBackedOff || loader.isLoading(), false); - - long now = SystemClock.elapsedRealtime(); - - if (isBackedOff) { - long elapsedMillis = now - currentLoadableExceptionTimestamp; - if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { - resumeFromBackOff(); - } - return; - } - - if (!loader.isLoading()) { - if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { - lastPerformedBufferOperation = now; - currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, - downstreamPositionUs, currentLoadableHolder); - discardUpstreamHlsChunks(currentLoadableHolder.queueSize); - } - if (nextLoader) { - maybeStartLoading(); - } - } - } - - /** - * Resumes loading. - *

- * If the {@link HlsChunkSource} returns a chunk equivalent to the backed off chunk B, then the - * loading of B will be resumed. In all other cases B will be discarded and the new chunk will - * be loaded. - */ - private void resumeFromBackOff() { - currentLoadableException = null; - - HlsChunk backedOffChunk = currentLoadableHolder.chunk; - if (!isTsChunk(backedOffChunk)) { - currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs, - currentLoadableHolder); - discardUpstreamHlsChunks(currentLoadableHolder.queueSize); - if (currentLoadableHolder.chunk == backedOffChunk) { - // HlsChunk was unchanged. Resume loading. - loader.startLoading(backedOffChunk, this); - } else { - backedOffChunk.release(); - maybeStartLoading(); - } - return; - } - - if (backedOffChunk == mediaChunks.getFirst()) { - // We're not able to clear the first media chunk, so we have no choice but to continue - // loading it. - loader.startLoading(backedOffChunk, this); - return; - } - - // The current loadable is the last media chunk. Remove it before we invoke the chunk source, - // and add it back again afterwards. - TsChunk removedChunk = mediaChunks.removeLast(); - Assertions.checkState(backedOffChunk == removedChunk); - currentLoadableHolder.queueSize = readOnlyHlsChunks.size(); - chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs, downstreamPositionUs, - currentLoadableHolder); - mediaChunks.add(removedChunk); - - if (currentLoadableHolder.chunk == backedOffChunk) { - // HlsChunk was unchanged. Resume loading. - loader.startLoading(backedOffChunk, this); - } else { - // This call will remove and release at least one chunk from the end of mediaChunks. Since - // the current loadable is the last media chunk, it is guaranteed to be removed. - discardUpstreamHlsChunks(currentLoadableHolder.queueSize); clearCurrentLoadable(); maybeStartLoading(); } } + private void clearCurrentLoadable() { + currentLoadable = null; + currentLoadableException = null; + currentLoadableExceptionCount = 0; + currentLoadableExceptionFatal = false; + } + private void maybeStartLoading() { - HlsChunk currentLoadable = currentLoadableHolder.chunk; - if (currentLoadable == null) { - // Nothing to load. + if (currentLoadableExceptionFatal || loadingFinished) { return; } - currentLoadable.init(loadControl.getAllocator()); + + boolean isBackedOff = currentLoadableException != null; + if (isBackedOff) { + long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; + if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { + currentLoadableException = null; + loader.startLoading(currentLoadable, this); + } + return; + } + + boolean bufferFull = !extractors.isEmpty() && (extractors.getLast().getLargestSampleTimestamp() + - downstreamPositionUs) >= BUFFER_DURATION_US; + if (loader.isLoading() || bufferFull) { + return; + } + + HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, + pendingResetPositionUs, downstreamPositionUs); + if (nextLoadable == null) { + return; + } + + currentLoadable = nextLoadable; if (isTsChunk(currentLoadable)) { - TsChunk mediaChunk = (TsChunk) currentLoadable; - mediaChunks.add(mediaChunk); + previousTsLoadable = (TsChunk) currentLoadable; if (isPendingReset()) { - discardExtractors(); pendingResetPositionUs = NO_RESET_PENDING; } + if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) { + extractors.addLast(previousTsLoadable.extractor); + } } loader.startLoading(currentLoadable, this); } @@ -534,39 +354,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { extractors.clear(); } - /** - * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not - * itself discarded. Null can be passed to discard all media chunks. - * - * @param untilChunk The first media chunk to keep, or null to discard all media chunks. - */ - private void discardDownstreamHlsChunks(TsChunk untilChunk) { - if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) { - return; - } - while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) { - mediaChunks.removeFirst().release(); - } - } - - /** - * Discards the first downstream media chunk. - */ - private void discardDownstreamHlsChunk() { - mediaChunks.removeFirst().release(); - } - - /** - * Discard upstream media chunks until the queue length is equal to the length specified. - * - * @param queueLength The desired length of the queue. - */ - private void discardUpstreamHlsChunks(int queueLength) { - while (mediaChunks.size() > queueLength) { - mediaChunks.removeLast().release(); - } - } - private boolean isTsChunk(HlsChunk chunk) { return chunk instanceof TsChunk; } 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 e69b8c88e0..9222317839 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,9 +15,12 @@ */ package com.google.android.exoplayer.hls; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; +import java.io.IOException; + /** * A MPEG2TS chunk. */ @@ -40,40 +43,87 @@ public final class TsChunk extends HlsChunk { */ public final int nextChunkIndex; /** - * The encoding discontinuity indicator. + * True if this is the final chunk being loaded for the current variant, as we splice to another + * one. False otherwise. */ - public final boolean discontinuity; + public final boolean splicingOut; /** - * For each track, whether samples from the first keyframe (inclusive) should be discarded. + * The extractor into which this chunk is being consumed. */ - public final boolean discardFromFirstKeyframes; + public final TsExtractor extractor; + + private volatile int loadPosition; + private volatile boolean loadFinished; + private volatile boolean loadCanceled; /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. - * @param trigger The reason for this chunk being selected. * @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 nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. - * @param discontinuity The encoding discontinuity indicator. - * @param discardFromFirstKeyframes For each contained media stream, whether samples from the - * first keyframe (inclusive) should be discarded. + * @param splicingOut True if this is the final chunk being loaded for the current variant, as we + * splice to another one. False otherwise. */ - public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex, - long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity, - boolean discardFromFirstKeyframes) { - super(dataSource, dataSpec, trigger); + public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor, + int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) { + super(dataSource, dataSpec); + this.extractor = tsExtractor; this.variantIndex = variantIndex; this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.nextChunkIndex = nextChunkIndex; - this.discontinuity = discontinuity; - this.discardFromFirstKeyframes = discardFromFirstKeyframes; + this.splicingOut = splicingOut; + } + + @Override + public void consume() throws IOException { + // Do nothing. } public boolean isLastChunk() { return nextChunkIndex == -1; } + @Override + public boolean isLoadFinished() { + return loadFinished; + } + + // Loadable implementation + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + DataSpec loadDataSpec; + if (loadPosition == 0) { + loadDataSpec = dataSpec; + } else { + long remainingLength = dataSpec.length != C.LENGTH_UNBOUNDED + ? dataSpec.length - loadPosition : C.LENGTH_UNBOUNDED; + loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, + remainingLength, dataSpec.key); + } + try { + dataSource.open(loadDataSpec); + int bytesRead = 0; + while (bytesRead != -1 && !loadCanceled) { + bytesRead = extractor.read(dataSource); + } + loadFinished = !loadCanceled; + } finally { + dataSource.close(); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java similarity index 72% rename from library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java rename to library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java index d305aa4f31..e2ccabf569 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsExtractor.java @@ -13,25 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.parser.ts; +package com.google.android.exoplayer.hls; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; import android.annotation.SuppressLint; -import android.media.MediaCodec; import android.media.MediaExtractor; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; +import java.util.List; import java.util.Queue; /** @@ -49,15 +51,15 @@ public final class TsExtractor { private static final int TS_STREAM_TYPE_H264 = 0x1B; private static final int TS_STREAM_TYPE_ID3 = 0x15; - private final BitsArray tsPacketBuffer; + private final BitArray tsPacketBuffer; private final SparseArray pesPayloadReaders; // Indexed by streamType private final SparseArray tsPayloadReaders; // Indexed by pid private final SamplePool samplePool; + /* package */ final long firstSampleTimestamp; private boolean prepared; /* package */ boolean pendingFirstSampleTimestampAdjustment; - /* package */ long firstSampleTimestamp; /* package */ long sampleTimestampOffsetUs; /* package */ long largestParsedTimestampUs; /* package */ boolean discardFromNextKeyframes; @@ -66,7 +68,7 @@ public final class TsExtractor { this.firstSampleTimestamp = firstSampleTimestamp; this.samplePool = samplePool; pendingFirstSampleTimestampAdjustment = true; - tsPacketBuffer = new BitsArray(); + tsPacketBuffer = new BitArray(); pesPayloadReaders = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); @@ -117,31 +119,19 @@ public final class TsExtractor { } /** - * For each track, whether to discard samples from the next keyframe (inclusive). + * For each track, discards samples from the next keyframe (inclusive). */ public void discardFromNextKeyframes() { discardFromNextKeyframes = true; } /** - * Consumes data from a {@link NonBlockingInputStream}. - *

- * The read terminates if the end of the input stream is reached, if insufficient data is - * available to read a sample, or if the extractor has consumed up to the specified target - * timestamp. + * Gets the largest timestamp of any sample parsed by the extractor. * - * @param inputStream The input stream from which data should be read. - * @param targetTimestampUs A target timestamp to consume up to. - * @return True if the target timestamp was reached. False otherwise. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. */ - public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) { - while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) { - // Carry on. - } - if (!prepared) { - prepared = checkPrepared(); - } - return largestParsedTimestampUs >= targetTimestampUs; + public long getLargestSampleTimestamp() { + return largestParsedTimestampUs; } /** @@ -204,23 +194,29 @@ public final class TsExtractor { } /** - * Read a single TS packet. + * 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. */ - private int readTSPacket(NonBlockingInputStream inputStream) { - // Read entire single TS packet. - if (inputStream.getAvailableByteCount() < TS_PACKET_SIZE) { + public int read(DataSource dataSource) throws IOException { + int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft()); + if (read == -1) { return -1; } - tsPacketBuffer.reset(); - tsPacketBuffer.append(inputStream, TS_PACKET_SIZE); + if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) { + return read; + } // Parse TS header. // Check sync byte. int syncByte = tsPacketBuffer.readUnsignedByte(); if (syncByte != TS_SYNC_BYTE) { - return 0; + return read; } + // Skip transportErrorIndicator. tsPacketBuffer.skipBits(1); boolean payloadUnitStartIndicator = tsPacketBuffer.readBit(); @@ -243,12 +239,17 @@ public final class TsExtractor { // Read Payload. if (payloadExists) { TsPayloadReader payloadReader = tsPayloadReaders.get(pid); - if (payloadReader == null) { - return 0; + if (payloadReader != null) { + payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); } - payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator); } - return 0; + + if (!prepared) { + prepared = checkPrepared(); + } + + tsPacketBuffer.reset(); + return read; } private void convert(Sample in, SampleHolder out) { @@ -268,7 +269,7 @@ public final class TsExtractor { */ private abstract static class TsPayloadReader { - public abstract void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator); + public abstract void read(BitArray tsBuffer, boolean payloadUnitStartIndicator); } @@ -278,7 +279,7 @@ public final class TsExtractor { private class PatReader extends TsPayloadReader { @Override - public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = tsBuffer.readBits(8); @@ -311,7 +312,7 @@ public final class TsExtractor { private class PmtReader extends TsPayloadReader { @Override - public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { // Skip pointer. if (payloadUnitStartIndicator) { int pointerField = tsBuffer.readBits(8); @@ -323,10 +324,10 @@ public final class TsExtractor { int sectionLength = tsBuffer.readBits(12); // Skip the rest of the PMT header. tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4 - int programInfoLength = tsBuffer.readBits(12); - // Read descriptors. - readDescriptors(tsBuffer, programInfoLength); + int programInfoLength = tsBuffer.readBits(12); + // Skip the descriptors. + tsBuffer.skipBytes(programInfoLength); int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */ - programInfoLength - 4 /* CRC size */; @@ -335,9 +336,10 @@ public final class TsExtractor { tsBuffer.skipBits(3); int elementaryPid = tsBuffer.readBits(13); tsBuffer.skipBits(4); - int esInfoLength = tsBuffer.readBits(12); - readDescriptors(tsBuffer, esInfoLength); + int esInfoLength = tsBuffer.readBits(12); + // Skip the descriptors. + tsBuffer.skipBytes(esInfoLength); entriesSize -= esInfoLength + 5; if (pesPayloadReaders.get(streamType) != null) { @@ -366,19 +368,6 @@ public final class TsExtractor { // Skip CRC_32. } - private void readDescriptors(BitsArray tsBuffer, int descriptorsSize) { - while (descriptorsSize > 0) { - // Skip tag. - tsBuffer.skipBits(8); - int descriptorsLength = tsBuffer.readBits(8); - if (descriptorsLength > 0) { - // Skip entire descriptor data. - tsBuffer.skipBytes(descriptorsLength); - } - descriptorsSize -= descriptorsSize + 2; - } - } - } /** @@ -387,7 +376,7 @@ public final class TsExtractor { private class PesReader extends TsPayloadReader { // Reusable buffer for incomplete PES data. - private final BitsArray pesBuffer; + private final BitArray pesBuffer; // Parses PES payload and extracts individual samples. private final PesPayloadReader pesPayloadReader; @@ -396,11 +385,11 @@ public final class TsExtractor { public PesReader(PesPayloadReader pesPayloadReader) { this.pesPayloadReader = pesPayloadReader; this.packetLength = -1; - pesBuffer = new BitsArray(); + pesBuffer = new BitArray(); } @Override - public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { + public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) { if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { // We've encountered the start of the next packet, but haven't yet read the body. Read it. // Note that this should only happen if the packet length was unspecified. @@ -484,7 +473,7 @@ public final class TsExtractor { */ private abstract class PesPayloadReader { - public final Queue sampleQueue; + public final LinkedList sampleQueue; private MediaFormat mediaFormat; private boolean foundFirstKeyframe; @@ -506,7 +495,7 @@ public final class TsExtractor { this.mediaFormat = mediaFormat; } - public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs); + public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs); public void clear() { while (!sampleQueue.isEmpty()) { @@ -521,7 +510,7 @@ public final class TsExtractor { * @param sampleSize The size of the sample data. * @param sampleTimeUs The sample time stamp. */ - protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) { + protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, int flags) { Sample sample = samplePool.get(); addToSample(sample, buffer, sampleSize); sample.flags = flags; @@ -531,7 +520,7 @@ public final class TsExtractor { @SuppressLint("InlinedApi") protected void addSample(Sample sample) { - boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; + boolean isKeyframe = (sample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0; if (isKeyframe) { if (!foundFirstKeyframe) { foundFirstKeyframe = true; @@ -549,7 +538,7 @@ public final class TsExtractor { } } - protected void addToSample(Sample sample, BitsArray buffer, int size) { + protected void addToSample(Sample sample, BitArray buffer, int size) { if (sample.data.length - sample.size < size) { sample.expand(size - sample.data.length + sample.size); } @@ -572,22 +561,24 @@ public final class TsExtractor { */ private class H264Reader extends PesPayloadReader { - // IDR picture. private static final int NAL_UNIT_TYPE_IDR = 5; - // Access unit delimiter. private static final int NAL_UNIT_TYPE_AUD = 9; + private static final int NAL_UNIT_TYPE_SPS = 7; // Used to store uncompleted sample data. private Sample currentSample; - public H264Reader() { - // TODO: Parse the format from the stream. - setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - 1920, 1080, null)); + @Override + public void clear() { + super.clear(); + if (currentSample != null) { + samplePool.recycle(currentSample); + currentSample = null; + } } @Override - public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { // Read leftover frame data from previous PES packet. pesPayloadSize -= readOneH264Frame(pesBuffer, true); @@ -597,6 +588,9 @@ public final class TsExtractor { // Single PES packet should contain only one new H.264 frame. if (currentSample != null) { + if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) { + parseMediaFormat(currentSample); + } addSample(currentSample); } currentSample = samplePool.get(); @@ -609,32 +603,120 @@ public final class TsExtractor { } @SuppressLint("InlinedApi") - private int readOneH264Frame(BitsArray pesBuffer, boolean remainderOnly) { + private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) { int offset = remainderOnly ? 0 : 3; int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); - int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); - if (audStart > 0) { - if (currentSample != null) { - addToSample(currentSample, pesBuffer, audStart); - if (idrStart < audStart) { - currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC; - } - } else { - pesBuffer.skipBytes(audStart); + if (currentSample != null) { + int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); + if (idrStart < audStart) { + currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC; } - return audStart; + addToSample(currentSample, pesBuffer, audStart); + } else { + pesBuffer.skipBytes(audStart); } - return 0; + return audStart; } - @Override - public void clear() { - super.clear(); - if (currentSample != null) { - samplePool.recycle(currentSample); - currentSample = null; + private void parseMediaFormat(Sample sample) { + BitArray bitArray = new BitArray(sample.data, sample.size); + // Locate the SPS unit. + int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0); + if (spsOffset == bitArray.bytesLeft()) { + return; } + int nextNalOffset = bitArray.findNextNalUnit(-1, spsOffset + 3); + + // Unescape the SPS unit. + byte[] unescapedSps = unescapeStream(bitArray.getData(), spsOffset, nextNalOffset); + bitArray.reset(unescapedSps, unescapedSps.length); + + // Parse the SPS unit + // Skip the NAL header. + bitArray.skipBytes(4); + // TODO: Handle different profiles properly. + bitArray.skipBytes(1); + // Skip 6 constraint bits, 2 reserved bits and level_idc. + bitArray.skipBytes(2); + // Skip seq_parameter_set_id. + bitArray.readExpGolombCodedInt(); + // Skip log2_max_frame_num_minus4 + bitArray.readExpGolombCodedInt(); + long picOrderCntType = bitArray.readExpGolombCodedInt(); + if (picOrderCntType == 0) { + // Skip log2_max_pic_order_cnt_lsb_minus4 + bitArray.readExpGolombCodedInt(); + } else if (picOrderCntType == 1) { + // Skip delta_pic_order_always_zero_flag + bitArray.skipBits(1); + // Skip offset_for_non_ref_pic (actually a signed value, but for skipping we can read it + // as though it were unsigned). + bitArray.readExpGolombCodedInt(); + // Skip offset_for_top_to_bottom_field (actually a signed value, but for skipping we can + // read it as though it were unsigned). + bitArray.readExpGolombCodedInt(); + long numRefFramesInPicOrderCntCycle = bitArray.readExpGolombCodedInt(); + for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { + // Skip offset_for_ref_frame[i] + bitArray.readExpGolombCodedInt(); + } + } + // Skip max_num_ref_frames + bitArray.readExpGolombCodedInt(); + // Skip gaps_in_frame_num_value_allowed_flag + bitArray.skipBits(1); + int picWidthInMbs = bitArray.readExpGolombCodedInt() + 1; + int picHeightInMapUnits = bitArray.readExpGolombCodedInt() + 1; + boolean frameMbsOnlyFlag = bitArray.readBit(); + int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; + + // Set the format. + setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + picWidthInMbs * 16, frameHeightInMbs * 16, null)); } + + /** + * Replaces occurrences of [0, 0, 3] with [0, 0]. + *

+ * See ISO/IEC 14496-10:2005(E) page 36 for more information. + */ + private byte[] unescapeStream(byte[] data, int offset, int limit) { + int position = offset; + List escapePositions = new ArrayList(); + while (position < limit) { + position = findNextUnescapeIndex(data, position, limit); + if (position < limit) { + escapePositions.add(position); + position += 3; + } + } + + int escapeCount = escapePositions.size(); + int escapedPosition = offset; // 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); + int copyLength = nextEscapePosition - escapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, copyLength); + escapedPosition += copyLength + 3; + unescapedPosition += copyLength + 2; + } + + int remainingLength = unescapedData.length - unescapedPosition; + System.arraycopy(data, escapedPosition, unescapedData, unescapedPosition, remainingLength); + return unescapedData; + } + + private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + for (int i = offset; i < limit - 2; i++) { + if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { + return i; + } + } + return limit; + } + } /** @@ -642,15 +724,15 @@ public final class TsExtractor { */ private class AdtsReader extends PesPayloadReader { - private final BitsArray adtsBuffer; + private final BitArray adtsBuffer; private long timeUs; public AdtsReader() { - adtsBuffer = new BitsArray(); + adtsBuffer = new BitArray(); } @Override - public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); adtsBuffer.append(pesBuffer, pesPayloadSize); // If there are leftovers from previous PES packet, process it with last calculated timeUs. @@ -751,7 +833,7 @@ public final class TsExtractor { @SuppressLint("InlinedApi") @Override - public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { + public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) { addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC); } @@ -764,31 +846,33 @@ public final class TsExtractor { private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024; - private final ArrayList samples; + private Sample firstInPool; - public SamplePool() { - samples = new ArrayList(); - } - - /* package */ Sample get() { - if (samples.isEmpty()) { + /* package */ synchronized Sample get() { + if (firstInPool == null) { return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); } - return samples.remove(samples.size() - 1); + Sample sample = firstInPool; + firstInPool = sample.nextInPool; + sample.nextInPool = null; + return sample; } - /* package */ void recycle(Sample sample) { + /* package */ synchronized void recycle(Sample sample) { sample.reset(); - samples.add(sample); + sample.nextInPool = firstInPool; + firstInPool = sample; } } /** - * Simplified version of SampleHolder for internal buffering. + * An internal variant of {@link SampleHolder} for internal pooling and buffering. */ private static class Sample { + public Sample nextInPool; + public byte[] data; public int flags; public int size; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/Variant.java b/library/src/main/java/com/google/android/exoplayer/hls/Variant.java new file mode 100644 index 0000000000..47d0a450a9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/Variant.java @@ -0,0 +1,56 @@ +/* + * 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; + +import java.util.Comparator; + +/** + * Variant stream reference. + */ +public final class Variant { + + /** + * Sorts {@link Variant} objects in order of decreasing bandwidth. + *

+ * When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first. + */ + public static final class DecreasingBandwidthComparator implements Comparator { + + @Override + public int compare(Variant a, Variant b) { + int bandwidthDifference = b.bandwidth - a.bandwidth; + return bandwidthDifference != 0 ? bandwidthDifference : a.index - b.index; + } + + } + + public final int index; + public final int bandwidth; + public final String url; + public final String[] codecs; + public final int width; + public final int height; + + public Variant(int index, String url, int bandwidth, String[] codecs, int width, int height) { + this.index = index; + this.bandwidth = bandwidth; + this.url = url; + this.codecs = codecs; + this.width = width; + this.height = height; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java index 518ee0c2bc..ea68984e37 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/Id3Parser.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer.metadata; import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.parser.ts.BitsArray; +import com.google.android.exoplayer.util.BitArray; import com.google.android.exoplayer.util.MimeTypes; import java.io.UnsupportedEncodingException; @@ -37,7 +37,7 @@ public class Id3Parser implements MetadataParser { @Override public Map parse(byte[] data, int size) throws UnsupportedEncodingException, ParserException { - BitsArray id3Buffer = new BitsArray(data, size); + BitArray id3Buffer = new BitArray(data, size); int id3Size = parseId3Header(id3Buffer); Map metadata = new HashMap(); @@ -102,11 +102,11 @@ public class Id3Parser implements MetadataParser { /** * Parses ID3 header. - * @param id3Buffer A {@link BitsArray} with raw ID3 data. + * @param id3Buffer A {@link BitArray} with raw ID3 data. * @return The size of data that contains ID3 frames without header and footer. * @throws ParserException If ID3 file identifier != "ID3". */ - private static int parseId3Header(BitsArray id3Buffer) throws ParserException { + private static int parseId3Header(BitArray id3Buffer) throws ParserException { int id1 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte(); diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java index 1f9b0dcaef..8bfaca8b77 100644 --- a/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java +++ b/library/src/main/java/com/google/android/exoplayer/metadata/MetadataParser.java @@ -19,7 +19,7 @@ import java.io.IOException; import java.util.Map; /** - * Parses {@link Metadata}s from binary data. + * Parses metadata objects from binary data. */ public interface MetadataParser { diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java similarity index 80% rename from library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java rename to library/src/main/java/com/google/android/exoplayer/util/BitArray.java index efd43dbec3..d5c26c0a2c 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/BitsArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/BitArray.java @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.parser.ts; +package com.google.android.exoplayer.util; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; -import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.upstream.DataSource; + +import java.io.IOException; /** * Wraps a byte array, providing methods that allow it to be read as a bitstream. */ -public final class BitsArray { +public final class BitArray { private byte[] data; @@ -33,16 +34,16 @@ public final class BitsArray { private int byteOffset; private int bitOffset; - public BitsArray() { + public BitArray() { } - public BitsArray(byte[] data, int limit) { + public BitArray(byte[] data, int limit) { this.data = data; this.limit = limit; } /** - * Resets the state. + * Clears all data, setting the offset and limit to zero. */ public void reset() { byteOffset = 0; @@ -50,6 +51,28 @@ public final class BitsArray { limit = 0; } + /** + * Resets to wrap the specified data, setting the offset to zero. + * + * @param data The data to wrap. + * @param limit The limit to set. + */ + public void reset(byte[] data, int limit) { + this.data = data; + this.limit = limit; + byteOffset = 0; + bitOffset = 0; + } + + /** + * Gets the backing byte array. + * + * @return The backing byte array. + */ + public byte[] getData() { + return data; + } + /** * Gets the current byte offset. * @@ -69,16 +92,16 @@ public final class BitsArray { } /** - * Appends data from a {@link NonBlockingInputStream}. + * Appends data from a {@link DataSource}. * - * @param inputStream The {@link NonBlockingInputStream} whose data should be appended. + * @param dataSource The {@link DataSource} from which to read. * @param length The maximum number of bytes to read and append. - * @return The number of bytes that were read and appended. May be 0 if no data was available - * from the stream. -1 is returned if the end of the stream has been reached. + * @return The number of bytes that were read and appended, or -1 if no more data is available. + * @throws IOException If an error occurs reading from the source. */ - public int append(NonBlockingInputStream inputStream, int length) { + public int append(DataSource dataSource, int length) throws IOException { expand(length); - int bytesRead = inputStream.read(data, limit, length); + int bytesRead = dataSource.read(data, limit, length); if (bytesRead == -1) { return -1; } @@ -87,12 +110,12 @@ public final class BitsArray { } /** - * Appends data from another {@link BitsArray}. + * Appends data from another {@link BitArray}. * - * @param bitsArray The {@link BitsArray} whose data should be appended. + * @param bitsArray The {@link BitArray} whose data should be appended. * @param length The number of bytes to read and append. */ - public void append(BitsArray bitsArray, int length) { + public void append(BitArray bitsArray, int length) { expand(length); bitsArray.readBytes(data, limit, length); limit += length; @@ -256,6 +279,19 @@ public final class BitsArray { return limit == 0; } + /** + * Reads an Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readExpGolombCodedInt() { + int leadingZeros = 0; + while (!readBit()) { + leadingZeros++; + } + return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); + } + /** * Reads a Synchsafe integer. * Synchsafe integers are integers that keep the highest bit of every byte zeroed. @@ -293,7 +329,7 @@ public final class BitsArray { /** * Finds the next NAL unit. * - * @param nalUnitType The type of the NAL unit to search for. + * @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit. * @param offset The additional offset in the data to start the search from. * @return The offset from the current position to the start of the NAL unit. If a NAL unit is * not found, then the offset to the end of the data is returned. @@ -302,7 +338,7 @@ public final class BitsArray { for (int i = byteOffset + offset; i < limit - 3; i++) { // Check for NAL unit start code prefix == 0x000001. if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) - && (nalUnitType == (data[i + 3] & 0x1F))) { + && (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) { return i - byteOffset; } }