Big HLS update. Add start of adaptive support, but leave disabled for now.

This commit is contained in:
Oliver Woodman 2014-11-13 16:32:10 +00:00
parent 6c6ba900a9
commit fd51901620
17 changed files with 776 additions and 724 deletions

View File

@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer.demo.full.player; 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.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; 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.demo.full.player.DemoPlayer.RendererBuilderCallback;
import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsMasterPlaylist; 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.HlsMasterPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource; 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.Id3Parser;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer; 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.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
@ -47,9 +45,6 @@ import java.util.Collections;
*/ */
public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<HlsMasterPlaylist> { public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<HlsMasterPlaylist> {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private final String userAgent; private final String userAgent;
private final String url; private final String url;
private final String contentId; private final String contentId;
@ -89,12 +84,12 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
@Override @Override
public void onManifest(String contentId, HlsMasterPlaylist manifest) { public void onManifest(String contentId, HlsMasterPlaylist manifest) {
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, null); DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest); HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null,
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, false);
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 3); HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, player.getMainHandler(), player, 50);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource); MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
@ -111,7 +106,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) {
return new HlsMasterPlaylist(Uri.parse(""), return new HlsMasterPlaylist(Uri.parse(""),
Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1))); Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1)));
} }
} }

View File

@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer.demo.simple; package com.google.android.exoplayer.demo.simple;
import com.google.android.exoplayer.DefaultLoadControl;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
@ -26,11 +24,11 @@ import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBui
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback; import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilderCallback;
import com.google.android.exoplayer.hls.HlsChunkSource; import com.google.android.exoplayer.hls.HlsChunkSource;
import com.google.android.exoplayer.hls.HlsMasterPlaylist; 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.HlsMasterPlaylistParser;
import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.hls.Variant;
import com.google.android.exoplayer.upstream.DataSource; 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.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
@ -47,9 +45,6 @@ import java.util.Collections;
/* package */ class HlsRendererBuilder implements RendererBuilder, /* package */ class HlsRendererBuilder implements RendererBuilder,
ManifestCallback<HlsMasterPlaylist> { ManifestCallback<HlsMasterPlaylist> {
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200;
private final SimplePlayerActivity playerActivity; private final SimplePlayerActivity playerActivity;
private final String userAgent; private final String userAgent;
private final String url; private final String url;
@ -90,12 +85,11 @@ import java.util.Collections;
@Override @Override
public void onManifest(String contentId, HlsMasterPlaylist manifest) { public void onManifest(String contentId, HlsMasterPlaylist manifest) {
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);
DataSource dataSource = new UriDataSource(userAgent, null); HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest, bandwidthMeter, null,
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, manifest); false);
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl, HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 2);
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, 2);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(), MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, playerActivity.getMainHandler(),
playerActivity, 50); playerActivity, 50);
@ -109,7 +103,7 @@ import java.util.Collections;
private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) { private HlsMasterPlaylist newSimpleMasterPlaylist(String mediaPlaylistUrl) {
return new HlsMasterPlaylist(Uri.parse(""), return new HlsMasterPlaylist(Uri.parse(""),
Collections.singletonList(new Variant(mediaPlaylistUrl, 0, null, -1, -1))); Collections.singletonList(new Variant(0, mediaPlaylistUrl, 0, null, -1, -1)));
} }
} }

View File

@ -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();
}
}
}

View File

@ -15,38 +15,21 @@
*/ */
package com.google.android.exoplayer.hls; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSourceStream;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import java.io.IOException; import java.io.IOException;
/** /**
* An abstract base class for {@link Loadable} implementations that load chunks of data required * An abstract base class for {@link Loadable} implementations that load chunks of data required
* for the playback of streams. * for the playback of HLS streams.
* <p>
* TODO: Figure out whether this should merge with the chunk package, or whether the hls
* implementation is going to naturally diverge.
*/ */
public abstract class HlsChunk implements Loadable { public abstract class HlsChunk implements Loadable {
/** protected final DataSource dataSource;
* The reason for a {@link HlsChunkSource} having generated this chunk. For reporting only. protected final DataSpec dataSpec;
* 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;
/** /**
* @param dataSource The source from which the data should be loaded. * @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 * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}. * {@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); Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = Assertions.checkNotNull(dataSource); this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec); this.dataSpec = Assertions.checkNotNull(dataSpec);
this.trigger = trigger;
} }
/** public abstract void consume() throws IOException;
* 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 boolean isLoadFinished();
* 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.
* <p>
* 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();
}
} }

View File

@ -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.
* <p>
* 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;
}

View File

@ -18,10 +18,12 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackRenderer; 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.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; 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 com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
@ -30,6 +32,8 @@ import android.os.SystemClock;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -41,26 +45,66 @@ import java.util.Locale;
*/ */
public class HlsChunkSource { 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 DataSource upstreamDataSource;
private final HlsMasterPlaylist masterPlaylist;
private final HlsMediaPlaylistParser mediaPlaylistParser; 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 */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ boolean mediaPlaylistWasLive; /* package */ final long[] lastMediaPlaylistLoadTimesMs;
/* package */ long lastMediaPlaylistLoadTimeMs; /* package */ boolean live;
/* package */ long durationUs;
private int variantIndex;
private DataSource encryptedDataSource; private DataSource encryptedDataSource;
private String encryptionKeyUri; 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.upstreamDataSource = dataSource;
this.masterPlaylist = masterPlaylist; this.bandwidthMeter = bandwidthMeter;
this.enableAdaptive = enableAdaptive;
baseUri = masterPlaylist.baseUri;
bitArray = new BitArray();
mediaPlaylistParser = new HlsMediaPlaylistParser(); 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() { 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. * @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
*/ */
public void getMaxVideoDimensions(MediaFormat out) { public void getMaxVideoDimensions(MediaFormat out) {
// TODO: Implement this. out.setMaxVideoDimensions(maxWidth, maxHeight);
} }
/** /**
* Updates the provided {@link HlsChunkOperationHolder} to contain the next operation that should * Returns the next {@link HlsChunk} that should be loaded.
* be performed by the calling {@link HlsSampleSource}.
* <p>
* 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.
* *
* @param queue A representation of the currently buffered {@link TsChunk}s. * @param previousTsChunk The previously loaded chunk that the next chunk should follow.
* @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
* the queue is non-empty then this parameter is ignored. * position. If there is a previous chunk then this parameter is ignored.
* @param playbackPositionUs The current playback position. * @param playbackPositionUs The current playback position.
* @param out A holder for the next operation, whose {@link HlsChunkOperationHolder#queueSize} is * @return The next chunk to load.
* 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.
*/ */
public void getChunkOperation(List<TsChunk> queue, long seekPositionUs, long playbackPositionUs, public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
HlsChunkOperationHolder out) { long playbackPositionUs) {
if (out.chunk != null) {
// We already have a chunk. Keep it.
return;
}
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
if (mediaPlaylist == null) { if (mediaPlaylist == null) {
out.chunk = newMediaPlaylistChunk(); return newMediaPlaylistChunk();
return;
} }
int chunkMediaSequence = 0; int chunkMediaSequence = 0;
if (mediaPlaylistWasLive) { if (live) {
if (queue.isEmpty()) { if (previousTsChunk == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence(); chunkMediaSequence = getLiveStartChunkMediaSequence();
} else { } else {
// For live nextChunkIndex contains chunk media sequence number. // 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 // 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. // queue, then try to catch up, skip a few chunks and start as if it was a new playlist.
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
@ -124,28 +152,26 @@ public class HlsChunkSource {
} }
} else { } else {
// Not live. // Not live.
if (queue.isEmpty()) { if (previousTsChunk == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
true) + mediaPlaylist.mediaSequence; true) + mediaPlaylist.mediaSequence;
} else { } else {
chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex; chunkMediaSequence = previousTsChunk.nextChunkIndex;
} }
} }
if (chunkMediaSequence == -1) { if (chunkMediaSequence == -1) {
out.chunk = null; // We've reached the end of the stream.
return; return null;
} }
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
// If the end of the playlist is reached.
if (chunkIndex >= mediaPlaylist.segments.size()) { if (chunkIndex >= mediaPlaylist.segments.size()) {
if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) { if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) {
out.chunk = newMediaPlaylistChunk(); return newMediaPlaylistChunk();
} else { } else {
out.chunk = null; return null;
} }
return;
} }
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
@ -156,97 +182,204 @@ public class HlsChunkSource {
if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) { if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed. // Encryption is specified and the key has changed.
Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV); HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
encryptionKeyUri = segment.encryptionKeyUri; encryptionKeyUri = segment.encryptionKeyUri;
return; return toReturn;
} }
} else { } else {
encryptedDataSource = null; encryptedDataSource = null;
encryptionKeyUri = null; encryptionKeyUri = null;
} }
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
long startTimeUs; long startTimeUs;
boolean splicingIn = previousTsChunk != null && previousTsChunk.splicingOut;
int nextChunkMediaSequence = chunkMediaSequence + 1; int nextChunkMediaSequence = chunkMediaSequence + 1;
if (mediaPlaylistWasLive) { if (live) {
if (queue.isEmpty()) { if (previousTsChunk == null) {
startTimeUs = 0; startTimeUs = 0;
} else if (splicingIn) {
startTimeUs = previousTsChunk.startTimeUs;
} else { } else {
startTimeUs = queue.get(queue.size() - 1).endTimeUs; startTimeUs = previousTsChunk.endTimeUs;
} }
} else { } else {
// Not live. // Not live.
startTimeUs = segment.startTimeUs; 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); 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; DataSource dataSource;
if (encryptedDataSource != null) { if (encryptedDataSource != null) {
dataSource = encryptedDataSource; dataSource = encryptedDataSource;
} else { } else {
dataSource = upstreamDataSource; dataSource = upstreamDataSource;
} }
out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs, DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
nextChunkMediaSequence, segment.discontinuity, false);
// 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() { private boolean shouldRerequestMediaPlaylist() {
// Don't re-request media playlist more often than one-half of the target duration. // Don't re-request media playlist more often than one-half of the target duration.
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
long timeSinceLastMediaPlaylistLoadMs = long timeSinceLastMediaPlaylistLoadMs =
SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimeMs; SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex];
return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2;
} }
private int getLiveStartChunkMediaSequence() { private int getLiveStartChunkMediaSequence() {
// For live start playback from the third chunk from the end. // 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; int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0;
return chunkIndex + mediaPlaylist.mediaSequence; return chunkIndex + mediaPlaylist.mediaSequence;
} }
private MediaPlaylistChunk newMediaPlaylistChunk() { private MediaPlaylistChunk newMediaPlaylistChunk() {
Uri mediaPlaylistUri = Util.getMergedUri(masterPlaylist.baseUri, Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
masterPlaylist.variants.get(0).url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
return new MediaPlaylistChunk(upstreamDataSource, dataSpec, 0, mediaPlaylistBaseUri); return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri);
} }
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null); 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<Variant> masterVariants = masterPlaylist.variants;
ArrayList<Variant> enabledVariants = new ArrayList<Variant>();
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<Variant> definiteVideoVariants = new ArrayList<Variant>();
ArrayList<Variant> definiteAudioOnlyVariants = new ArrayList<Variant>();
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) { if (!definiteVideoVariants.isEmpty()) {
super(dataSource, dataSpec, trigger); // We've identified some variants as definitely containing video. Assume variants within the
this.baseUri = baseUri; // 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 @Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException { protected void consume(BitArray data) throws IOException {
byte[] data = new byte[(int) stream.getAvailableByteCount()]; HlsMediaPlaylist mediaPlaylist = mediaPlaylistParser.parse(
stream.read(data, 0, data.length); new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null,
lastMediaPlaylistLoadTimeMs = SystemClock.elapsedRealtime(); playlistBaseUri);
mediaPlaylist = mediaPlaylistParser.parse( mediaPlaylists[variantIndex] = mediaPlaylist;
new ByteArrayInputStream(data), null, null, baseUri); lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime();
mediaPlaylistWasLive |= mediaPlaylist.live; live |= mediaPlaylist.live;
durationUs = mediaPlaylist.durationUs;
} }
} }
private class EncryptionKeyChunk extends HlsChunk { private class EncryptionKeyChunk extends BitArrayChunk {
private final String iv; private final String iv;
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, int trigger, String iv) { public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) {
super(dataSource, dataSpec, trigger); super(dataSource, dataSpec, bitArray);
if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
this.iv = iv.substring(2); this.iv = iv.substring(2);
} else { } else {
@ -255,9 +388,9 @@ public class HlsChunkSource {
} }
@Override @Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException { protected void consume(BitArray data) throws IOException {
byte[] keyData = new byte[(int) stream.getAvailableByteCount()]; byte[] secretKey = new byte[data.bytesLeft()];
stream.read(keyData, 0, keyData.length); data.readBytes(secretKey, 0, secretKey.length);
int ivParsed = Integer.parseInt(iv, 16); int ivParsed = Integer.parseInt(iv, 16);
String iv = String.format("%032X", ivParsed); String iv = String.format("%032X", ivParsed);
@ -267,7 +400,7 @@ public class HlsChunkSource {
System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length, System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length,
ivData.length); ivData.length);
encryptedDataSource = new Aes128DataSource(keyData, ivDataWithPadding, upstreamDataSource); encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource);
} }
} }

View File

@ -24,25 +24,6 @@ import java.util.List;
*/ */
public final class HlsMasterPlaylist { 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 Uri baseUri;
public final List<Variant> variants; public final List<Variant> variants;

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.hls.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer.util.ManifestParser; import com.google.android.exoplayer.util.ManifestParser;
import android.net.Uri; import android.net.Uri;
@ -61,6 +60,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
String[] codecs = null; String[] codecs = null;
int width = -1; int width = -1;
int height = -1; int height = -1;
int variantIndex = 0;
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
@ -70,15 +70,14 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
} }
if (line.startsWith(STREAM_INF_TAG)) { if (line.startsWith(STREAM_INF_TAG)) {
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX, String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
CODECS_ATTR);
if (codecsString != null) { if (codecsString != null) {
codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)"); codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
} else { } else {
codecs = null; codecs = null;
} }
String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, RESOLUTION_ATTR_REGEX, String resolutionString = HlsParserUtil.parseOptionalStringAttr(line,
RESOLUTION_ATTR); RESOLUTION_ATTR_REGEX);
if (resolutionString != null) { if (resolutionString != null) {
String[] widthAndHeight = resolutionString.split("x"); String[] widthAndHeight = resolutionString.split("x");
width = Integer.parseInt(widthAndHeight[0]); width = Integer.parseInt(widthAndHeight[0]);
@ -88,7 +87,7 @@ public final class HlsMasterPlaylistParser implements ManifestParser<HlsMasterPl
height = -1; height = -1;
} }
} else if (!line.startsWith("#")) { } else if (!line.startsWith("#")) {
variants.add(new Variant(line, bandwidth, codecs, width, height)); variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
bandwidth = 0; bandwidth = 0;
codecs = null; codecs = null;
width = -1; width = -1;

View File

@ -114,8 +114,7 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
} else { } else {
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
URI_ATTR); URI_ATTR);
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX, segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
IV_ATTR);
if (segmentEncryptionIV == null) { if (segmentEncryptionIV == null) {
segmentEncryptionIV = Integer.toHexString(segmentMediaSequence); segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
} }

View File

@ -36,7 +36,7 @@ import java.util.regex.Pattern;
throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line)); throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
} }
public static String parseOptionalStringAttr(String line, Pattern pattern, String tag) { public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line); Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) { if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1); return matcher.group(1);

View File

@ -15,47 +15,31 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.parser.ts.TsExtractor;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import android.os.SystemClock; import android.os.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
/** /**
* A {@link SampleSource} for HLS streams. * A {@link SampleSource} for HLS streams.
* <p>
* 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 { 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 static final int NO_RESET_PENDING = -1;
private final TsExtractor.SamplePool samplePool;
private final LoadControl loadControl;
private final HlsChunkSource chunkSource; private final HlsChunkSource chunkSource;
private final HlsChunkOperationHolder currentLoadableHolder;
private final LinkedList<TsExtractor> extractors; private final LinkedList<TsExtractor> extractors;
private final LinkedList<TsChunk> mediaChunks;
private final List<TsChunk> readOnlyHlsChunks;
private final int bufferSizeContribution;
private final boolean frameAccurateSeeking; private final boolean frameAccurateSeeking;
private int remainingReleaseCount; private int remainingReleaseCount;
@ -70,7 +54,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private long downstreamPositionUs; private long downstreamPositionUs;
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingResetPositionUs; private long pendingResetPositionUs;
private long lastPerformedBufferOperation;
private TsChunk previousTsLoadable;
private HlsChunk currentLoadable;
private boolean loadingFinished;
private Loader loader; private Loader loader;
private IOException currentLoadableException; private IOException currentLoadableException;
@ -78,18 +65,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp; private long currentLoadableExceptionTimestamp;
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, public HlsSampleSource(HlsChunkSource chunkSource,
int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { boolean frameAccurateSeeking, int downstreamRendererCount) {
this.chunkSource = chunkSource; this.chunkSource = chunkSource;
this.loadControl = loadControl;
this.bufferSizeContribution = bufferSizeContribution;
this.frameAccurateSeeking = frameAccurateSeeking; this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount; this.remainingReleaseCount = downstreamRendererCount;
samplePool = new TsExtractor.SamplePool();
extractors = new LinkedList<TsExtractor>(); extractors = new LinkedList<TsExtractor>();
currentLoadableHolder = new HlsChunkOperationHolder();
mediaChunks = new LinkedList<TsChunk>();
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
} }
@Override @Override
@ -99,13 +80,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
if (loader == null) { if (loader == null) {
loader = new Loader("Loader:HLS"); loader = new Loader("Loader:HLS");
loadControl.register(this, bufferSizeContribution);
} }
continueBufferingInternal(); continueBufferingInternal();
if (extractors.isEmpty()) { if (extractors.isEmpty()) {
return false; return false;
} }
TsExtractor extractor = extractors.get(0); TsExtractor extractor = extractors.getFirst();
if (extractor.isPrepared()) { if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount(); trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount];
@ -156,8 +136,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
clearHlsChunks(); discardExtractors();
clearCurrentLoadable(); clearCurrentLoadable();
previousTsLoadable = null;
} }
} }
} }
@ -171,50 +152,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
} }
private boolean continueBufferingInternal() throws IOException { private boolean continueBufferingInternal() throws IOException {
updateLoadControl(); maybeStartLoading();
if (isPendingReset()) { if (isPendingReset() || extractors.isEmpty()) {
return false; return false;
} }
boolean haveSamples = extractors.getFirst().hasSamples();
TsChunk mediaChunk = mediaChunks.getFirst(); if (!haveSamples && currentLoadableException != null) {
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) {
throw currentLoadableException; throw currentLoadableException;
} }
return haveSufficientSamples; return haveSamples;
} }
@Override @Override
@ -228,11 +174,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return DISCONTINUITY_READ; return DISCONTINUITY_READ;
} }
if (onlyReadDiscontinuity || isPendingReset()) { if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
return NOTHING_READ;
}
if (extractors.isEmpty()) {
return NOTHING_READ; return NOTHING_READ;
} }
@ -266,12 +208,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return SAMPLE_READ; return SAMPLE_READ;
} }
TsChunk mediaChunk = mediaChunks.getFirst(); return loadingFinished ? END_OF_STREAM : NOTHING_READ;
if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) {
return END_OF_STREAM;
}
return NOTHING_READ;
} }
@Override @Override
@ -283,32 +220,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (pendingResetPositionUs == positionUs) { if (pendingResetPositionUs == positionUs) {
return; return;
} }
for (int i = 0; i < pendingDiscontinuities.length; i++) { for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true; pendingDiscontinuities[i] = true;
} }
TsChunk mediaChunk = getHlsChunk(positionUs); restartFrom(positionUs);
if (mediaChunk == null) {
restartFrom(positionUs);
} else {
discardExtractors();
discardDownstreamHlsChunks(mediaChunk);
mediaChunk.resetReadPosition();
updateLoadControl();
}
}
private TsChunk getHlsChunk(long positionUs) {
Iterator<TsChunk> 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;
} }
@Override @Override
@ -317,22 +232,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(enabledTrackCount > 0); Assertions.checkState(enabledTrackCount > 0);
if (isPendingReset()) { if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} } else if (loadingFinished) {
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()) {
return TrackRenderer.END_OF_TRACK_US; return TrackRenderer.END_OF_TRACK_US;
} else { } else {
return mediaChunk.endTimeUs; return extractors.getLast().getLargestSampleTimestamp();
} }
} }
@ -340,7 +243,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
public void release() { public void release() {
Assertions.checkState(remainingReleaseCount > 0); Assertions.checkState(remainingReleaseCount > 0);
if (--remainingReleaseCount == 0 && loader != null) { if (--remainingReleaseCount == 0 && loader != null) {
loadControl.unregister(this);
loader.release(); loader.release();
loader = null; loader = null;
} }
@ -348,7 +250,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
HlsChunk currentLoadable = currentLoadableHolder.chunk;
try { try {
currentLoadable.consume(); currentLoadable.consume();
} catch (IOException e) { } catch (IOException e) {
@ -357,28 +258,24 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
currentLoadableExceptionFatal = true; currentLoadableExceptionFatal = true;
} finally { } finally {
if (!isTsChunk(currentLoadable)) { if (isTsChunk(currentLoadable)) {
currentLoadable.release(); TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk();
} }
if (!currentLoadableExceptionFatal) { if (!currentLoadableExceptionFatal) {
clearCurrentLoadable(); clearCurrentLoadable();
} }
updateLoadControl(); maybeStartLoading();
} }
} }
@Override @Override
public void onLoadCanceled(Loadable loadable) { public void onLoadCanceled(Loadable loadable) {
HlsChunk currentLoadable = currentLoadableHolder.chunk;
if (!isTsChunk(currentLoadable)) {
currentLoadable.release();
}
clearCurrentLoadable(); clearCurrentLoadable();
if (enabledTrackCount > 0) { if (enabledTrackCount > 0) {
restartFrom(pendingResetPositionUs); restartFrom(pendingResetPositionUs);
} else { } else {
clearHlsChunks(); previousTsLoadable = null;
loadControl.trimAllocator();
} }
} }
@ -387,142 +284,65 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
currentLoadableException = e; currentLoadableException = e;
currentLoadableExceptionCount++; currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
updateLoadControl(); maybeStartLoading();
} }
private void restartFrom(long positionUs) { private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
previousTsLoadable = null;
loadingFinished = false;
discardExtractors();
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} else { } 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.
* <p>
* 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(); clearCurrentLoadable();
maybeStartLoading(); maybeStartLoading();
} }
} }
private void clearCurrentLoadable() {
currentLoadable = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
currentLoadableExceptionFatal = false;
}
private void maybeStartLoading() { private void maybeStartLoading() {
HlsChunk currentLoadable = currentLoadableHolder.chunk; if (currentLoadableExceptionFatal || loadingFinished) {
if (currentLoadable == null) {
// Nothing to load.
return; 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)) { if (isTsChunk(currentLoadable)) {
TsChunk mediaChunk = (TsChunk) currentLoadable; previousTsLoadable = (TsChunk) currentLoadable;
mediaChunks.add(mediaChunk);
if (isPendingReset()) { if (isPendingReset()) {
discardExtractors();
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
} }
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) {
extractors.addLast(previousTsLoadable.extractor);
}
} }
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
} }
@ -534,39 +354,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
extractors.clear(); 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) { private boolean isTsChunk(HlsChunk chunk) {
return chunk instanceof TsChunk; return chunk instanceof TsChunk;
} }

View File

@ -15,9 +15,12 @@
*/ */
package com.google.android.exoplayer.hls; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import java.io.IOException;
/** /**
* A MPEG2TS chunk. * A MPEG2TS chunk.
*/ */
@ -40,40 +43,87 @@ public final class TsChunk extends HlsChunk {
*/ */
public final int nextChunkIndex; 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 dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @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 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 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 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 nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param discontinuity The encoding discontinuity indicator. * @param splicingOut True if this is the final chunk being loaded for the current variant, as we
* @param discardFromFirstKeyframes For each contained media stream, whether samples from the * splice to another one. False otherwise.
* first keyframe (inclusive) should be discarded.
*/ */
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex, public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor,
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity, int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) {
boolean discardFromFirstKeyframes) { super(dataSource, dataSpec);
super(dataSource, dataSpec, trigger); this.extractor = tsExtractor;
this.variantIndex = variantIndex; this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs; this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex; this.nextChunkIndex = nextChunkIndex;
this.discontinuity = discontinuity; this.splicingOut = splicingOut;
this.discardFromFirstKeyframes = discardFromFirstKeyframes; }
@Override
public void consume() throws IOException {
// Do nothing.
} }
public boolean isLastChunk() { public boolean isLastChunk() {
return nextChunkIndex == -1; 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();
}
}
} }

View File

@ -13,25 +13,27 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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.MediaFormat;
import com.google.android.exoplayer.SampleHolder; 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.Assertions;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaExtractor; import android.media.MediaExtractor;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.Queue; 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_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15; private static final int TS_STREAM_TYPE_ID3 = 0x15;
private final BitsArray tsPacketBuffer; private final BitArray tsPacketBuffer;
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final SamplePool samplePool; private final SamplePool samplePool;
/* package */ final long firstSampleTimestamp;
private boolean prepared; private boolean prepared;
/* package */ boolean pendingFirstSampleTimestampAdjustment; /* package */ boolean pendingFirstSampleTimestampAdjustment;
/* package */ long firstSampleTimestamp;
/* package */ long sampleTimestampOffsetUs; /* package */ long sampleTimestampOffsetUs;
/* package */ long largestParsedTimestampUs; /* package */ long largestParsedTimestampUs;
/* package */ boolean discardFromNextKeyframes; /* package */ boolean discardFromNextKeyframes;
@ -66,7 +68,7 @@ public final class TsExtractor {
this.firstSampleTimestamp = firstSampleTimestamp; this.firstSampleTimestamp = firstSampleTimestamp;
this.samplePool = samplePool; this.samplePool = samplePool;
pendingFirstSampleTimestampAdjustment = true; pendingFirstSampleTimestampAdjustment = true;
tsPacketBuffer = new BitsArray(); tsPacketBuffer = new BitArray();
pesPayloadReaders = new SparseArray<PesPayloadReader>(); pesPayloadReaders = new SparseArray<PesPayloadReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>(); tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader()); 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() { public void discardFromNextKeyframes() {
discardFromNextKeyframes = true; discardFromNextKeyframes = true;
} }
/** /**
* Consumes data from a {@link NonBlockingInputStream}. * Gets the largest timestamp of any sample parsed by the extractor.
* <p>
* 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.
* *
* @param inputStream The input stream from which data should be read. * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
* @param targetTimestampUs A target timestamp to consume up to.
* @return True if the target timestamp was reached. False otherwise.
*/ */
public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) { public long getLargestSampleTimestamp() {
while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) { return largestParsedTimestampUs;
// Carry on.
}
if (!prepared) {
prepared = checkPrepared();
}
return largestParsedTimestampUs >= targetTimestampUs;
} }
/** /**
@ -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) { public int read(DataSource dataSource) throws IOException {
// Read entire single TS packet. int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft());
if (inputStream.getAvailableByteCount() < TS_PACKET_SIZE) { if (read == -1) {
return -1; return -1;
} }
tsPacketBuffer.reset(); if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) {
tsPacketBuffer.append(inputStream, TS_PACKET_SIZE); return read;
}
// Parse TS header. // Parse TS header.
// Check sync byte. // Check sync byte.
int syncByte = tsPacketBuffer.readUnsignedByte(); int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) { if (syncByte != TS_SYNC_BYTE) {
return 0; return read;
} }
// Skip transportErrorIndicator. // Skip transportErrorIndicator.
tsPacketBuffer.skipBits(1); tsPacketBuffer.skipBits(1);
boolean payloadUnitStartIndicator = tsPacketBuffer.readBit(); boolean payloadUnitStartIndicator = tsPacketBuffer.readBit();
@ -243,12 +239,17 @@ public final class TsExtractor {
// Read Payload. // Read Payload.
if (payloadExists) { if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid); TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader == null) { if (payloadReader != null) {
return 0; 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) { private void convert(Sample in, SampleHolder out) {
@ -268,7 +269,7 @@ public final class TsExtractor {
*/ */
private abstract static class TsPayloadReader { 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 { private class PatReader extends TsPayloadReader {
@Override @Override
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = tsBuffer.readBits(8); int pointerField = tsBuffer.readBits(8);
@ -311,7 +312,7 @@ public final class TsExtractor {
private class PmtReader extends TsPayloadReader { private class PmtReader extends TsPayloadReader {
@Override @Override
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
// Skip pointer. // Skip pointer.
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int pointerField = tsBuffer.readBits(8); int pointerField = tsBuffer.readBits(8);
@ -323,10 +324,10 @@ public final class TsExtractor {
int sectionLength = tsBuffer.readBits(12); int sectionLength = tsBuffer.readBits(12);
// Skip the rest of the PMT header. // Skip the rest of the PMT header.
tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4 tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4
int programInfoLength = tsBuffer.readBits(12);
// Read descriptors. int programInfoLength = tsBuffer.readBits(12);
readDescriptors(tsBuffer, programInfoLength); // Skip the descriptors.
tsBuffer.skipBytes(programInfoLength);
int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */ int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */
- programInfoLength - 4 /* CRC size */; - programInfoLength - 4 /* CRC size */;
@ -335,9 +336,10 @@ public final class TsExtractor {
tsBuffer.skipBits(3); tsBuffer.skipBits(3);
int elementaryPid = tsBuffer.readBits(13); int elementaryPid = tsBuffer.readBits(13);
tsBuffer.skipBits(4); 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; entriesSize -= esInfoLength + 5;
if (pesPayloadReaders.get(streamType) != null) { if (pesPayloadReaders.get(streamType) != null) {
@ -366,19 +368,6 @@ public final class TsExtractor {
// Skip CRC_32. // 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 { private class PesReader extends TsPayloadReader {
// Reusable buffer for incomplete PES data. // Reusable buffer for incomplete PES data.
private final BitsArray pesBuffer; private final BitArray pesBuffer;
// Parses PES payload and extracts individual samples. // Parses PES payload and extracts individual samples.
private final PesPayloadReader pesPayloadReader; private final PesPayloadReader pesPayloadReader;
@ -396,11 +385,11 @@ public final class TsExtractor {
public PesReader(PesPayloadReader pesPayloadReader) { public PesReader(PesPayloadReader pesPayloadReader) {
this.pesPayloadReader = pesPayloadReader; this.pesPayloadReader = pesPayloadReader;
this.packetLength = -1; this.packetLength = -1;
pesBuffer = new BitsArray(); pesBuffer = new BitArray();
} }
@Override @Override
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) { public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) { if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) {
// We've encountered the start of the next packet, but haven't yet read the body. Read it. // 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. // Note that this should only happen if the packet length was unspecified.
@ -484,7 +473,7 @@ public final class TsExtractor {
*/ */
private abstract class PesPayloadReader { private abstract class PesPayloadReader {
public final Queue<Sample> sampleQueue; public final LinkedList<Sample> sampleQueue;
private MediaFormat mediaFormat; private MediaFormat mediaFormat;
private boolean foundFirstKeyframe; private boolean foundFirstKeyframe;
@ -506,7 +495,7 @@ public final class TsExtractor {
this.mediaFormat = mediaFormat; 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() { public void clear() {
while (!sampleQueue.isEmpty()) { while (!sampleQueue.isEmpty()) {
@ -521,7 +510,7 @@ public final class TsExtractor {
* @param sampleSize The size of the sample data. * @param sampleSize The size of the sample data.
* @param sampleTimeUs The sample time stamp. * @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(); Sample sample = samplePool.get();
addToSample(sample, buffer, sampleSize); addToSample(sample, buffer, sampleSize);
sample.flags = flags; sample.flags = flags;
@ -531,7 +520,7 @@ public final class TsExtractor {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
protected void addSample(Sample sample) { 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 (isKeyframe) {
if (!foundFirstKeyframe) { if (!foundFirstKeyframe) {
foundFirstKeyframe = true; 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) { if (sample.data.length - sample.size < size) {
sample.expand(size - sample.data.length + sample.size); sample.expand(size - sample.data.length + sample.size);
} }
@ -572,22 +561,24 @@ public final class TsExtractor {
*/ */
private class H264Reader extends PesPayloadReader { private class H264Reader extends PesPayloadReader {
// IDR picture.
private static final int NAL_UNIT_TYPE_IDR = 5; 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_AUD = 9;
private static final int NAL_UNIT_TYPE_SPS = 7;
// Used to store uncompleted sample data. // Used to store uncompleted sample data.
private Sample currentSample; private Sample currentSample;
public H264Reader() { @Override
// TODO: Parse the format from the stream. public void clear() {
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, super.clear();
1920, 1080, null)); if (currentSample != null) {
samplePool.recycle(currentSample);
currentSample = null;
}
} }
@Override @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. // Read leftover frame data from previous PES packet.
pesPayloadSize -= readOneH264Frame(pesBuffer, true); pesPayloadSize -= readOneH264Frame(pesBuffer, true);
@ -597,6 +588,9 @@ public final class TsExtractor {
// Single PES packet should contain only one new H.264 frame. // Single PES packet should contain only one new H.264 frame.
if (currentSample != null) { if (currentSample != null) {
if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
parseMediaFormat(currentSample);
}
addSample(currentSample); addSample(currentSample);
} }
currentSample = samplePool.get(); currentSample = samplePool.get();
@ -609,32 +603,120 @@ public final class TsExtractor {
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private int readOneH264Frame(BitsArray pesBuffer, boolean remainderOnly) { private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) {
int offset = remainderOnly ? 0 : 3; int offset = remainderOnly ? 0 : 3;
int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset); int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset);
int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset); if (currentSample != null) {
if (audStart > 0) { int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset);
if (currentSample != null) { if (idrStart < audStart) {
addToSample(currentSample, pesBuffer, audStart); currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC;
if (idrStart < audStart) {
currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC;
}
} else {
pesBuffer.skipBytes(audStart);
} }
return audStart; addToSample(currentSample, pesBuffer, audStart);
} else {
pesBuffer.skipBytes(audStart);
} }
return 0; return audStart;
} }
@Override private void parseMediaFormat(Sample sample) {
public void clear() { BitArray bitArray = new BitArray(sample.data, sample.size);
super.clear(); // Locate the SPS unit.
if (currentSample != null) { int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0);
samplePool.recycle(currentSample); if (spsOffset == bitArray.bytesLeft()) {
currentSample = null; 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].
* <p>
* 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<Integer> escapePositions = new ArrayList<Integer>();
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 class AdtsReader extends PesPayloadReader {
private final BitsArray adtsBuffer; private final BitArray adtsBuffer;
private long timeUs; private long timeUs;
public AdtsReader() { public AdtsReader() {
adtsBuffer = new BitsArray(); adtsBuffer = new BitArray();
} }
@Override @Override
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
boolean needToProcessLeftOvers = !adtsBuffer.isEmpty(); boolean needToProcessLeftOvers = !adtsBuffer.isEmpty();
adtsBuffer.append(pesBuffer, pesPayloadSize); adtsBuffer.append(pesBuffer, pesPayloadSize);
// If there are leftovers from previous PES packet, process it with last calculated timeUs. // If there are leftovers from previous PES packet, process it with last calculated timeUs.
@ -751,7 +833,7 @@ public final class TsExtractor {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
@Override @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); 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 static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
private final ArrayList<Sample> samples; private Sample firstInPool;
public SamplePool() { /* package */ synchronized Sample get() {
samples = new ArrayList<Sample>(); if (firstInPool == null) {
}
/* package */ Sample get() {
if (samples.isEmpty()) {
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE); 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(); 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 { private static class Sample {
public Sample nextInPool;
public byte[] data; public byte[] data;
public int flags; public int flags;
public int size; public int size;

View File

@ -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.
* <p>
* When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first.
*/
public static final class DecreasingBandwidthComparator implements Comparator<Variant> {
@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;
}
}

View File

@ -16,7 +16,7 @@
package com.google.android.exoplayer.metadata; package com.google.android.exoplayer.metadata;
import com.google.android.exoplayer.ParserException; 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 com.google.android.exoplayer.util.MimeTypes;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -37,7 +37,7 @@ public class Id3Parser implements MetadataParser {
@Override @Override
public Map<String, Object> parse(byte[] data, int size) public Map<String, Object> parse(byte[] data, int size)
throws UnsupportedEncodingException, ParserException { throws UnsupportedEncodingException, ParserException {
BitsArray id3Buffer = new BitsArray(data, size); BitArray id3Buffer = new BitArray(data, size);
int id3Size = parseId3Header(id3Buffer); int id3Size = parseId3Header(id3Buffer);
Map<String, Object> metadata = new HashMap<String, Object>(); Map<String, Object> metadata = new HashMap<String, Object>();
@ -102,11 +102,11 @@ public class Id3Parser implements MetadataParser {
/** /**
* Parses ID3 header. * 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. * @return The size of data that contains ID3 frames without header and footer.
* @throws ParserException If ID3 file identifier != "ID3". * @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 id1 = id3Buffer.readUnsignedByte();
int id2 = id3Buffer.readUnsignedByte(); int id2 = id3Buffer.readUnsignedByte();
int id3 = id3Buffer.readUnsignedByte(); int id3 = id3Buffer.readUnsignedByte();

View File

@ -19,7 +19,7 @@ import java.io.IOException;
import java.util.Map; import java.util.Map;
/** /**
* Parses {@link Metadata}s from binary data. * Parses metadata objects from binary data.
*/ */
public interface MetadataParser { public interface MetadataParser {

View File

@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
/** /**
* Wraps a byte array, providing methods that allow it to be read as a bitstream. * 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; private byte[] data;
@ -33,16 +34,16 @@ public final class BitsArray {
private int byteOffset; private int byteOffset;
private int bitOffset; private int bitOffset;
public BitsArray() { public BitArray() {
} }
public BitsArray(byte[] data, int limit) { public BitArray(byte[] data, int limit) {
this.data = data; this.data = data;
this.limit = limit; this.limit = limit;
} }
/** /**
* Resets the state. * Clears all data, setting the offset and limit to zero.
*/ */
public void reset() { public void reset() {
byteOffset = 0; byteOffset = 0;
@ -50,6 +51,28 @@ public final class BitsArray {
limit = 0; 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. * 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. * @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 * @return The number of bytes that were read and appended, or -1 if no more data is available.
* from the stream. -1 is returned if the end of the stream has been reached. * @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); expand(length);
int bytesRead = inputStream.read(data, limit, length); int bytesRead = dataSource.read(data, limit, length);
if (bytesRead == -1) { if (bytesRead == -1) {
return -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. * @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); expand(length);
bitsArray.readBytes(data, limit, length); bitsArray.readBytes(data, limit, length);
limit += length; limit += length;
@ -256,6 +279,19 @@ public final class BitsArray {
return limit == 0; 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. * Reads a Synchsafe integer.
* Synchsafe integers are integers that keep the highest bit of every byte zeroed. * 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. * 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. * @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 * @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. * 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++) { for (int i = byteOffset + offset; i < limit - 3; i++) {
// Check for NAL unit start code prefix == 0x000001. // Check for NAL unit start code prefix == 0x000001.
if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) 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; return i - byteOffset;
} }
} }