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

View File

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

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

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.MediaFormat;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.hls.TsExtractor.SamplePool;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
@ -30,6 +32,8 @@ import android.os.SystemClock;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -41,26 +45,66 @@ import java.util.Locale;
*/
public class HlsChunkSource {
private static final float BANDWIDTH_FRACTION = 0.8f;
private static final long MIN_BUFFER_TO_SWITCH_UP_US = 5000000;
private static final long MAX_BUFFER_TO_SWITCH_DOWN_US = 15000000;
private final SamplePool samplePool = new TsExtractor.SamplePool();
private final DataSource upstreamDataSource;
private final HlsMasterPlaylist masterPlaylist;
private final HlsMediaPlaylistParser mediaPlaylistParser;
private final Variant[] enabledVariants;
private final BandwidthMeter bandwidthMeter;
private final BitArray bitArray;
private final boolean enableAdaptive;
private final Uri baseUri;
private final int maxWidth;
private final int maxHeight;
/* package */ HlsMediaPlaylist mediaPlaylist;
/* package */ boolean mediaPlaylistWasLive;
/* package */ long lastMediaPlaylistLoadTimeMs;
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ final long[] lastMediaPlaylistLoadTimesMs;
/* package */ boolean live;
/* package */ long durationUs;
private int variantIndex;
private DataSource encryptedDataSource;
private String encryptionKeyUri;
// TODO: Once proper m3u8 parsing is in place, actually use the url!
public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) {
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param masterPlaylist The master playlist.
* @param variantIndices A subset of variant indices to consider, or null to consider all of the
* variants in the master playlist.
*/
public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist,
BandwidthMeter bandwidthMeter, int[] variantIndices, boolean enableAdaptive) {
this.upstreamDataSource = dataSource;
this.masterPlaylist = masterPlaylist;
this.bandwidthMeter = bandwidthMeter;
this.enableAdaptive = enableAdaptive;
baseUri = masterPlaylist.baseUri;
bitArray = new BitArray();
mediaPlaylistParser = new HlsMediaPlaylistParser();
enabledVariants = filterVariants(masterPlaylist, variantIndices);
lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length];
mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length];
int maxWidth = -1;
int maxHeight = -1;
// Select the first variant from the master playlist that's enabled.
long minOriginalVariantIndex = Integer.MAX_VALUE;
for (int i = 0; i < enabledVariants.length; i++) {
if (enabledVariants[i].index < minOriginalVariantIndex) {
minOriginalVariantIndex = enabledVariants[i].index;
variantIndex = i;
}
maxWidth = Math.max(enabledVariants[i].width, maxWidth);
maxHeight = Math.max(enabledVariants[i].width, maxHeight);
}
// TODO: We should allow the default values to be passed through the constructor.
this.maxWidth = maxWidth > 0 ? maxWidth : 1920;
this.maxHeight = maxHeight > 0 ? maxHeight : 1080;
}
public long getDurationUs() {
return mediaPlaylistWasLive ? TrackRenderer.UNKNOWN_TIME_US : mediaPlaylist.durationUs;
return live ? TrackRenderer.UNKNOWN_TIME_US : durationUs;
}
/**
@ -72,49 +116,33 @@ public class HlsChunkSource {
* @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
*/
public void getMaxVideoDimensions(MediaFormat out) {
// TODO: Implement this.
out.setMaxVideoDimensions(maxWidth, maxHeight);
}
/**
* Updates the provided {@link HlsChunkOperationHolder} to contain the next operation that should
* be performed by the calling {@link HlsSampleSource}.
* <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.
* Returns the next {@link HlsChunk} that should be loaded.
*
* @param queue A representation of the currently buffered {@link TsChunk}s.
* @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If
* the queue is non-empty then this parameter is ignored.
* @param previousTsChunk The previously loaded chunk that the next chunk should follow.
* @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
* position. If there is a previous chunk then this parameter is ignored.
* @param playbackPositionUs The current playback position.
* @param out A holder for the next operation, whose {@link HlsChunkOperationHolder#queueSize} is
* initially equal to the length of the queue, and whose {@linkHls ChunkOperationHolder#chunk}
* is initially equal to null or a {@link TsChunk} previously supplied by the
* {@link HlsChunkSource} that the caller has not yet finished loading. In the latter case the
* chunk can either be replaced or left unchanged. Note that leaving the chunk unchanged is
* both preferred and more efficient than replacing it with a new but identical chunk.
* @return The next chunk to load.
*/
public void getChunkOperation(List<TsChunk> queue, long seekPositionUs, long playbackPositionUs,
HlsChunkOperationHolder out) {
if (out.chunk != null) {
// We already have a chunk. Keep it.
return;
}
public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) {
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
if (mediaPlaylist == null) {
out.chunk = newMediaPlaylistChunk();
return;
return newMediaPlaylistChunk();
}
int chunkMediaSequence = 0;
if (mediaPlaylistWasLive) {
if (queue.isEmpty()) {
if (live) {
if (previousTsChunk == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence();
} else {
// For live nextChunkIndex contains chunk media sequence number.
chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex;
chunkMediaSequence = previousTsChunk.nextChunkIndex;
// If the updated playlist is far ahead and doesn't even have the last chunk from the
// queue, then try to catch up, skip a few chunks and start as if it was a new playlist.
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
@ -124,28 +152,26 @@ public class HlsChunkSource {
}
} else {
// Not live.
if (queue.isEmpty()) {
if (previousTsChunk == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
true) + mediaPlaylist.mediaSequence;
} else {
chunkMediaSequence = queue.get(queue.size() - 1).nextChunkIndex;
chunkMediaSequence = previousTsChunk.nextChunkIndex;
}
}
if (chunkMediaSequence == -1) {
out.chunk = null;
return;
// We've reached the end of the stream.
return null;
}
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
// If the end of the playlist is reached.
if (chunkIndex >= mediaPlaylist.segments.size()) {
if (mediaPlaylist.live && shouldRerequestMediaPlaylist()) {
out.chunk = newMediaPlaylistChunk();
return newMediaPlaylistChunk();
} else {
out.chunk = null;
return null;
}
return;
}
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
@ -156,97 +182,204 @@ public class HlsChunkSource {
if (!segment.encryptionKeyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed.
Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
encryptionKeyUri = segment.encryptionKeyUri;
return;
return toReturn;
}
} else {
encryptedDataSource = null;
encryptionKeyUri = null;
}
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
long startTimeUs;
boolean splicingIn = previousTsChunk != null && previousTsChunk.splicingOut;
int nextChunkMediaSequence = chunkMediaSequence + 1;
if (mediaPlaylistWasLive) {
if (queue.isEmpty()) {
if (live) {
if (previousTsChunk == null) {
startTimeUs = 0;
} else if (splicingIn) {
startTimeUs = previousTsChunk.startTimeUs;
} else {
startTimeUs = queue.get(queue.size() - 1).endTimeUs;
startTimeUs = previousTsChunk.endTimeUs;
}
} else {
// Not live.
startTimeUs = segment.startTimeUs;
if (chunkIndex == mediaPlaylist.segments.size() - 1) {
nextChunkMediaSequence = -1;
}
}
if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) {
nextChunkMediaSequence = -1;
}
long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000);
int currentVariantIndex = variantIndex;
boolean splicingOut = false;
if (splicingIn) {
// Do nothing.
} else if (enableAdaptive && nextChunkMediaSequence != -1) {
int idealVariantIndex = getVariantIndexForBandwdith(
(int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION));
long bufferedUs = startTimeUs - playbackPositionUs;
if ((idealVariantIndex > currentVariantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US)
|| (idealVariantIndex < currentVariantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) {
variantIndex = idealVariantIndex;
}
splicingOut = variantIndex != currentVariantIndex;
if (splicingOut) {
// If we're splicing out, we want to load the same chunk again next time, but for a
// different variant.
nextChunkMediaSequence = chunkMediaSequence;
}
}
// Configure the datasource for loading the chunk.
DataSource dataSource;
if (encryptedDataSource != null) {
dataSource = encryptedDataSource;
} else {
dataSource = upstreamDataSource;
}
out.chunk = new TsChunk(dataSource, dataSpec, 0, 0, startTimeUs, endTimeUs,
nextChunkMediaSequence, segment.discontinuity, false);
DataSpec dataSpec = new DataSpec(chunkUri, 0, C.LENGTH_UNBOUNDED, null);
// Configure the extractor that will read the chunk.
TsExtractor extractor;
if (previousTsChunk == null || splicingIn || segment.discontinuity) {
extractor = new TsExtractor(startTimeUs, samplePool);
} else {
extractor = previousTsChunk.extractor;
}
if (splicingOut) {
extractor.discardFromNextKeyframes();
}
return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[currentVariantIndex].index,
startTimeUs, endTimeUs, nextChunkMediaSequence, splicingOut);
}
private int getVariantIndexForBandwdith(int bandwidth) {
for (int i = 0; i < enabledVariants.length - 1; i++) {
if (enabledVariants[i].bandwidth <= bandwidth) {
return i;
}
}
return enabledVariants.length - 1;
}
private boolean shouldRerequestMediaPlaylist() {
// Don't re-request media playlist more often than one-half of the target duration.
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
long timeSinceLastMediaPlaylistLoadMs =
SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimeMs;
SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex];
return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2;
}
private int getLiveStartChunkMediaSequence() {
// For live start playback from the third chunk from the end.
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0;
return chunkIndex + mediaPlaylist.mediaSequence;
}
private MediaPlaylistChunk newMediaPlaylistChunk() {
Uri mediaPlaylistUri = Util.getMergedUri(masterPlaylist.baseUri,
masterPlaylist.variants.get(0).url);
Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
Uri mediaPlaylistBaseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
return new MediaPlaylistChunk(upstreamDataSource, dataSpec, 0, mediaPlaylistBaseUri);
Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri);
}
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null);
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, 0, iv);
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv);
}
private class MediaPlaylistChunk extends HlsChunk {
private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) {
List<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) {
super(dataSource, dataSpec, trigger);
this.baseUri = baseUri;
if (!definiteVideoVariants.isEmpty()) {
// We've identified some variants as definitely containing video. Assume variants within the
// master playlist are marked consistently, and hence that we have the full set. Filter out
// any other variants, which are likely to be audio only.
enabledVariants = definiteVideoVariants;
} else if (definiteAudioOnlyVariants.size() < enabledVariants.size()) {
// We've identified some variants, but not all, as being audio only. Filter them out to leave
// the remaining variants, which are likely to contain video.
enabledVariants.removeAll(definiteAudioOnlyVariants);
} else {
// Leave the enabled variants unchanged. They're likely either all video or all audio.
}
Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator());
Variant[] enabledVariantsArray = new Variant[enabledVariants.size()];
enabledVariants.toArray(enabledVariantsArray);
return enabledVariantsArray;
}
private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) {
String[] codecs = variant.codecs;
if (codecs == null) {
return false;
}
for (int i = 0; i < codecs.length; i++) {
if (codecs[i].startsWith(prefix)) {
return true;
}
}
return false;
}
private class MediaPlaylistChunk extends BitArrayChunk {
@SuppressWarnings("hiding")
private final int variantIndex;
private final Uri playlistBaseUri;
public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec,
Uri playlistBaseUri) {
super(dataSource, dataSpec, bitArray);
this.variantIndex = variantIndex;
this.playlistBaseUri = playlistBaseUri;
}
@Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
byte[] data = new byte[(int) stream.getAvailableByteCount()];
stream.read(data, 0, data.length);
lastMediaPlaylistLoadTimeMs = SystemClock.elapsedRealtime();
mediaPlaylist = mediaPlaylistParser.parse(
new ByteArrayInputStream(data), null, null, baseUri);
mediaPlaylistWasLive |= mediaPlaylist.live;
protected void consume(BitArray data) throws IOException {
HlsMediaPlaylist mediaPlaylist = mediaPlaylistParser.parse(
new ByteArrayInputStream(data.getData(), 0, data.bytesLeft()), null, null,
playlistBaseUri);
mediaPlaylists[variantIndex] = mediaPlaylist;
lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime();
live |= mediaPlaylist.live;
durationUs = mediaPlaylist.durationUs;
}
}
private class EncryptionKeyChunk extends HlsChunk {
private class EncryptionKeyChunk extends BitArrayChunk {
private final String iv;
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, int trigger, String iv) {
super(dataSource, dataSpec, trigger);
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) {
super(dataSource, dataSpec, bitArray);
if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
this.iv = iv.substring(2);
} else {
@ -255,9 +388,9 @@ public class HlsChunkSource {
}
@Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
byte[] keyData = new byte[(int) stream.getAvailableByteCount()];
stream.read(keyData, 0, keyData.length);
protected void consume(BitArray data) throws IOException {
byte[] secretKey = new byte[data.bytesLeft()];
data.readBytes(secretKey, 0, secretKey.length);
int ivParsed = Integer.parseInt(iv, 16);
String iv = String.format("%032X", ivParsed);
@ -267,7 +400,7 @@ public class HlsChunkSource {
System.arraycopy(ivData, 0, ivDataWithPadding, ivDataWithPadding.length - ivData.length,
ivData.length);
encryptedDataSource = new Aes128DataSource(keyData, ivDataWithPadding, upstreamDataSource);
encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource);
}
}

View File

@ -24,25 +24,6 @@ import java.util.List;
*/
public final class HlsMasterPlaylist {
/**
* Variant stream reference.
*/
public static final class Variant {
public final int bandwidth;
public final String url;
public final String[] codecs;
public final int width;
public final int height;
public Variant(String url, int bandwidth, String[] codecs, int width, int height) {
this.bandwidth = bandwidth;
this.url = url;
this.codecs = codecs;
this.width = width;
this.height = height;
}
}
public final Uri baseUri;
public final List<Variant> variants;

View File

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

View File

@ -114,8 +114,7 @@ public final class HlsMediaPlaylistParser implements ManifestParser<HlsMediaPlay
} else {
segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
URI_ATTR);
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX,
IV_ATTR);
segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
if (segmentEncryptionIV == null) {
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));
}
public static String parseOptionalStringAttr(String line, Pattern pattern, String tag) {
public static String parseOptionalStringAttr(String line, Pattern pattern) {
Matcher matcher = pattern.matcher(line);
if (matcher.find() && matcher.groupCount() == 1) {
return matcher.group(1);

View File

@ -15,47 +15,31 @@
*/
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.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.parser.ts.TsExtractor;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import android.os.SystemClock;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* 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 {
private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000;
private static final long BUFFER_DURATION_US = 20000000;
private static final int NO_RESET_PENDING = -1;
private final TsExtractor.SamplePool samplePool;
private final LoadControl loadControl;
private final HlsChunkSource chunkSource;
private final HlsChunkOperationHolder currentLoadableHolder;
private final LinkedList<TsExtractor> extractors;
private final LinkedList<TsChunk> mediaChunks;
private final List<TsChunk> readOnlyHlsChunks;
private final int bufferSizeContribution;
private final boolean frameAccurateSeeking;
private int remainingReleaseCount;
@ -70,7 +54,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private long downstreamPositionUs;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
private long lastPerformedBufferOperation;
private TsChunk previousTsLoadable;
private HlsChunk currentLoadable;
private boolean loadingFinished;
private Loader loader;
private IOException currentLoadableException;
@ -78,18 +65,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp;
public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl,
int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) {
public HlsSampleSource(HlsChunkSource chunkSource,
boolean frameAccurateSeeking, int downstreamRendererCount) {
this.chunkSource = chunkSource;
this.loadControl = loadControl;
this.bufferSizeContribution = bufferSizeContribution;
this.frameAccurateSeeking = frameAccurateSeeking;
this.remainingReleaseCount = downstreamRendererCount;
samplePool = new TsExtractor.SamplePool();
extractors = new LinkedList<TsExtractor>();
currentLoadableHolder = new HlsChunkOperationHolder();
mediaChunks = new LinkedList<TsChunk>();
readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks);
}
@Override
@ -99,13 +80,12 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
if (loader == null) {
loader = new Loader("Loader:HLS");
loadControl.register(this, bufferSizeContribution);
}
continueBufferingInternal();
if (extractors.isEmpty()) {
return false;
}
TsExtractor extractor = extractors.get(0);
TsExtractor extractor = extractors.getFirst();
if (extractor.isPrepared()) {
trackCount = extractor.getTrackCount();
trackEnabledStates = new boolean[trackCount];
@ -156,8 +136,9 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearHlsChunks();
discardExtractors();
clearCurrentLoadable();
previousTsLoadable = null;
}
}
}
@ -171,50 +152,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
private boolean continueBufferingInternal() throws IOException {
updateLoadControl();
if (isPendingReset()) {
maybeStartLoading();
if (isPendingReset() || extractors.isEmpty()) {
return false;
}
TsChunk mediaChunk = mediaChunks.getFirst();
int currentVariant = mediaChunk.variantIndex;
TsExtractor extractor;
if (extractors.isEmpty()) {
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
extractors.addLast(extractor);
if (mediaChunk.discardFromFirstKeyframes) {
extractor.discardFromNextKeyframes();
}
} else {
extractor = extractors.getLast();
}
if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) {
discardDownstreamHlsChunk();
mediaChunk = mediaChunks.getFirst();
if (mediaChunk.discontinuity || mediaChunk.variantIndex != currentVariant) {
extractor = new TsExtractor(mediaChunk.startTimeUs, samplePool);
extractors.addLast(extractor);
}
if (mediaChunk.discardFromFirstKeyframes) {
extractor.discardFromNextKeyframes();
}
}
// Allow the extractor to consume from the current chunk.
NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream();
boolean haveSufficientSamples = extractor.consumeUntil(inputStream,
downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US);
if (!haveSufficientSamples) {
// If we can't read any more, then we always say we have sufficient samples.
haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished();
}
if (!haveSufficientSamples && currentLoadableException != null) {
boolean haveSamples = extractors.getFirst().hasSamples();
if (!haveSamples && currentLoadableException != null) {
throw currentLoadableException;
}
return haveSufficientSamples;
return haveSamples;
}
@Override
@ -228,11 +174,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return DISCONTINUITY_READ;
}
if (onlyReadDiscontinuity || isPendingReset()) {
return NOTHING_READ;
}
if (extractors.isEmpty()) {
if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
return NOTHING_READ;
}
@ -266,12 +208,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return SAMPLE_READ;
}
TsChunk mediaChunk = mediaChunks.getFirst();
if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) {
return END_OF_STREAM;
}
return NOTHING_READ;
return loadingFinished ? END_OF_STREAM : NOTHING_READ;
}
@Override
@ -283,32 +220,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (pendingResetPositionUs == positionUs) {
return;
}
for (int i = 0; i < pendingDiscontinuities.length; i++) {
pendingDiscontinuities[i] = true;
}
TsChunk mediaChunk = getHlsChunk(positionUs);
if (mediaChunk == null) {
restartFrom(positionUs);
} else {
discardExtractors();
discardDownstreamHlsChunks(mediaChunk);
mediaChunk.resetReadPosition();
updateLoadControl();
}
}
private TsChunk getHlsChunk(long positionUs) {
Iterator<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;
restartFrom(positionUs);
}
@Override
@ -317,22 +232,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(enabledTrackCount > 0);
if (isPendingReset()) {
return pendingResetPositionUs;
}
TsChunk mediaChunk = mediaChunks.getLast();
HlsChunk currentLoadable = currentLoadableHolder.chunk;
if (currentLoadable != null && mediaChunk == currentLoadable) {
// Linearly interpolate partially-fetched chunk times.
long chunkLength = mediaChunk.getLength();
if (chunkLength != C.LENGTH_UNBOUNDED) {
return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs)
* mediaChunk.bytesLoaded()) / chunkLength;
} else {
return mediaChunk.startTimeUs;
}
} else if (mediaChunk.isLastChunk()) {
} else if (loadingFinished) {
return TrackRenderer.END_OF_TRACK_US;
} else {
return mediaChunk.endTimeUs;
return extractors.getLast().getLargestSampleTimestamp();
}
}
@ -340,7 +243,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
public void release() {
Assertions.checkState(remainingReleaseCount > 0);
if (--remainingReleaseCount == 0 && loader != null) {
loadControl.unregister(this);
loader.release();
loader = null;
}
@ -348,7 +250,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
@Override
public void onLoadCompleted(Loadable loadable) {
HlsChunk currentLoadable = currentLoadableHolder.chunk;
try {
currentLoadable.consume();
} catch (IOException e) {
@ -357,28 +258,24 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
currentLoadableExceptionFatal = true;
} finally {
if (!isTsChunk(currentLoadable)) {
currentLoadable.release();
if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk();
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
updateLoadControl();
maybeStartLoading();
}
}
@Override
public void onLoadCanceled(Loadable loadable) {
HlsChunk currentLoadable = currentLoadableHolder.chunk;
if (!isTsChunk(currentLoadable)) {
currentLoadable.release();
}
clearCurrentLoadable();
if (enabledTrackCount > 0) {
restartFrom(pendingResetPositionUs);
} else {
clearHlsChunks();
loadControl.trimAllocator();
previousTsLoadable = null;
}
}
@ -387,142 +284,65 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
updateLoadControl();
maybeStartLoading();
}
private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs;
previousTsLoadable = null;
loadingFinished = false;
discardExtractors();
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearHlsChunks();
clearCurrentLoadable();
updateLoadControl();
}
}
private void clearHlsChunks() {
discardDownstreamHlsChunks(null);
}
private void clearCurrentLoadable() {
currentLoadableHolder.chunk = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
currentLoadableExceptionFatal = false;
}
private void updateLoadControl() {
if (currentLoadableExceptionFatal) {
// We've failed, but we still need to update the control with our current state.
loadControl.update(this, downstreamPositionUs, -1, false, true);
return;
}
long loadPositionUs;
if (isPendingReset()) {
loadPositionUs = pendingResetPositionUs;
} else {
TsChunk lastHlsChunk = mediaChunks.getLast();
loadPositionUs = lastHlsChunk.nextChunkIndex == -1 ? -1 : lastHlsChunk.endTimeUs;
}
boolean isBackedOff = currentLoadableException != null;
boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs,
isBackedOff || loader.isLoading(), false);
long now = SystemClock.elapsedRealtime();
if (isBackedOff) {
long elapsedMillis = now - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
resumeFromBackOff();
}
return;
}
if (!loader.isLoading()) {
if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) {
lastPerformedBufferOperation = now;
currentLoadableHolder.queueSize = readOnlyHlsChunks.size();
chunkSource.getChunkOperation(readOnlyHlsChunks, pendingResetPositionUs,
downstreamPositionUs, currentLoadableHolder);
discardUpstreamHlsChunks(currentLoadableHolder.queueSize);
}
if (nextLoader) {
maybeStartLoading();
}
}
}
/**
* Resumes loading.
* <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();
maybeStartLoading();
}
}
private void clearCurrentLoadable() {
currentLoadable = null;
currentLoadableException = null;
currentLoadableExceptionCount = 0;
currentLoadableExceptionFatal = false;
}
private void maybeStartLoading() {
HlsChunk currentLoadable = currentLoadableHolder.chunk;
if (currentLoadable == null) {
// Nothing to load.
if (currentLoadableExceptionFatal || loadingFinished) {
return;
}
currentLoadable.init(loadControl.getAllocator());
boolean isBackedOff = currentLoadableException != null;
if (isBackedOff) {
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
currentLoadableException = null;
loader.startLoading(currentLoadable, this);
}
return;
}
boolean bufferFull = !extractors.isEmpty() && (extractors.getLast().getLargestSampleTimestamp()
- downstreamPositionUs) >= BUFFER_DURATION_US;
if (loader.isLoading() || bufferFull) {
return;
}
HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
pendingResetPositionUs, downstreamPositionUs);
if (nextLoadable == null) {
return;
}
currentLoadable = nextLoadable;
if (isTsChunk(currentLoadable)) {
TsChunk mediaChunk = (TsChunk) currentLoadable;
mediaChunks.add(mediaChunk);
previousTsLoadable = (TsChunk) currentLoadable;
if (isPendingReset()) {
discardExtractors();
pendingResetPositionUs = NO_RESET_PENDING;
}
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) {
extractors.addLast(previousTsLoadable.extractor);
}
}
loader.startLoading(currentLoadable, this);
}
@ -534,39 +354,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
extractors.clear();
}
/**
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
* itself discarded. Null can be passed to discard all media chunks.
*
* @param untilChunk The first media chunk to keep, or null to discard all media chunks.
*/
private void discardDownstreamHlsChunks(TsChunk untilChunk) {
if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) {
return;
}
while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) {
mediaChunks.removeFirst().release();
}
}
/**
* Discards the first downstream media chunk.
*/
private void discardDownstreamHlsChunk() {
mediaChunks.removeFirst().release();
}
/**
* Discard upstream media chunks until the queue length is equal to the length specified.
*
* @param queueLength The desired length of the queue.
*/
private void discardUpstreamHlsChunks(int queueLength) {
while (mediaChunks.size() > queueLength) {
mediaChunks.removeLast().release();
}
}
private boolean isTsChunk(HlsChunk chunk) {
return chunk instanceof TsChunk;
}

View File

@ -15,9 +15,12 @@
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import java.io.IOException;
/**
* A MPEG2TS chunk.
*/
@ -40,40 +43,87 @@ public final class TsChunk extends HlsChunk {
*/
public final int nextChunkIndex;
/**
* The encoding discontinuity indicator.
* True if this is the final chunk being loaded for the current variant, as we splice to another
* one. False otherwise.
*/
public final boolean discontinuity;
public final boolean splicingOut;
/**
* For each track, whether samples from the first keyframe (inclusive) should be discarded.
* The extractor into which this chunk is being consumed.
*/
public final boolean discardFromFirstKeyframes;
public final TsExtractor extractor;
private volatile int loadPosition;
private volatile boolean loadFinished;
private volatile boolean loadCanceled;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param trigger The reason for this chunk being selected.
* @param variantIndex The index of the variant in the master playlist.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param discontinuity The encoding discontinuity indicator.
* @param discardFromFirstKeyframes For each contained media stream, whether samples from the
* first keyframe (inclusive) should be discarded.
* @param splicingOut True if this is the final chunk being loaded for the current variant, as we
* splice to another one. False otherwise.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, int variantIndex,
long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity,
boolean discardFromFirstKeyframes) {
super(dataSource, dataSpec, trigger);
public TsChunk(DataSource dataSource, DataSpec dataSpec, TsExtractor tsExtractor,
int variantIndex, long startTimeUs, long endTimeUs, int nextChunkIndex, boolean splicingOut) {
super(dataSource, dataSpec);
this.extractor = tsExtractor;
this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex;
this.discontinuity = discontinuity;
this.discardFromFirstKeyframes = discardFromFirstKeyframes;
this.splicingOut = splicingOut;
}
@Override
public void consume() throws IOException {
// Do nothing.
}
public boolean isLastChunk() {
return nextChunkIndex == -1;
}
@Override
public boolean isLoadFinished() {
return loadFinished;
}
// Loadable implementation
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
public void load() throws IOException, InterruptedException {
DataSpec loadDataSpec;
if (loadPosition == 0) {
loadDataSpec = dataSpec;
} else {
long remainingLength = dataSpec.length != C.LENGTH_UNBOUNDED
? dataSpec.length - loadPosition : C.LENGTH_UNBOUNDED;
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
remainingLength, dataSpec.key);
}
try {
dataSource.open(loadDataSpec);
int bytesRead = 0;
while (bytesRead != -1 && !loadCanceled) {
bytesRead = extractor.read(dataSource);
}
loadFinished = !loadCanceled;
} finally {
dataSource.close();
}
}
}

View File

@ -13,25 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.parser.ts;
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.BitArray;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
@ -49,15 +51,15 @@ public final class TsExtractor {
private static final int TS_STREAM_TYPE_H264 = 0x1B;
private static final int TS_STREAM_TYPE_ID3 = 0x15;
private final BitsArray tsPacketBuffer;
private final BitArray tsPacketBuffer;
private final SparseArray<PesPayloadReader> pesPayloadReaders; // Indexed by streamType
private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
private final SamplePool samplePool;
/* package */ final long firstSampleTimestamp;
private boolean prepared;
/* package */ boolean pendingFirstSampleTimestampAdjustment;
/* package */ long firstSampleTimestamp;
/* package */ long sampleTimestampOffsetUs;
/* package */ long largestParsedTimestampUs;
/* package */ boolean discardFromNextKeyframes;
@ -66,7 +68,7 @@ public final class TsExtractor {
this.firstSampleTimestamp = firstSampleTimestamp;
this.samplePool = samplePool;
pendingFirstSampleTimestampAdjustment = true;
tsPacketBuffer = new BitsArray();
tsPacketBuffer = new BitArray();
pesPayloadReaders = new SparseArray<PesPayloadReader>();
tsPayloadReaders = new SparseArray<TsPayloadReader>();
tsPayloadReaders.put(TS_PAT_PID, new PatReader());
@ -117,31 +119,19 @@ public final class TsExtractor {
}
/**
* For each track, whether to discard samples from the next keyframe (inclusive).
* For each track, discards samples from the next keyframe (inclusive).
*/
public void discardFromNextKeyframes() {
discardFromNextKeyframes = true;
}
/**
* Consumes data from a {@link NonBlockingInputStream}.
* <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.
* Gets the largest timestamp of any sample parsed by the extractor.
*
* @param inputStream The input stream from which data should be read.
* @param targetTimestampUs A target timestamp to consume up to.
* @return True if the target timestamp was reached. False otherwise.
* @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed.
*/
public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) {
while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) {
// Carry on.
}
if (!prepared) {
prepared = checkPrepared();
}
return largestParsedTimestampUs >= targetTimestampUs;
public long getLargestSampleTimestamp() {
return largestParsedTimestampUs;
}
/**
@ -204,23 +194,29 @@ public final class TsExtractor {
}
/**
* Read a single TS packet.
* Reads up to a single TS packet.
*
* @param dataSource The {@link DataSource} from which to read.
* @throws IOException If an error occurred reading from the source.
* @return The number of bytes read from the source.
*/
private int readTSPacket(NonBlockingInputStream inputStream) {
// Read entire single TS packet.
if (inputStream.getAvailableByteCount() < TS_PACKET_SIZE) {
public int read(DataSource dataSource) throws IOException {
int read = tsPacketBuffer.append(dataSource, TS_PACKET_SIZE - tsPacketBuffer.bytesLeft());
if (read == -1) {
return -1;
}
tsPacketBuffer.reset();
tsPacketBuffer.append(inputStream, TS_PACKET_SIZE);
if (tsPacketBuffer.bytesLeft() != TS_PACKET_SIZE) {
return read;
}
// Parse TS header.
// Check sync byte.
int syncByte = tsPacketBuffer.readUnsignedByte();
if (syncByte != TS_SYNC_BYTE) {
return 0;
return read;
}
// Skip transportErrorIndicator.
tsPacketBuffer.skipBits(1);
boolean payloadUnitStartIndicator = tsPacketBuffer.readBit();
@ -243,12 +239,17 @@ public final class TsExtractor {
// Read Payload.
if (payloadExists) {
TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
if (payloadReader == null) {
return 0;
if (payloadReader != null) {
payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator);
}
payloadReader.read(tsPacketBuffer, payloadUnitStartIndicator);
}
return 0;
if (!prepared) {
prepared = checkPrepared();
}
tsPacketBuffer.reset();
return read;
}
private void convert(Sample in, SampleHolder out) {
@ -268,7 +269,7 @@ public final class TsExtractor {
*/
private abstract static class TsPayloadReader {
public abstract void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator);
public abstract void read(BitArray tsBuffer, boolean payloadUnitStartIndicator);
}
@ -278,7 +279,7 @@ public final class TsExtractor {
private class PatReader extends TsPayloadReader {
@Override
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) {
public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
// Skip pointer.
if (payloadUnitStartIndicator) {
int pointerField = tsBuffer.readBits(8);
@ -311,7 +312,7 @@ public final class TsExtractor {
private class PmtReader extends TsPayloadReader {
@Override
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) {
public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
// Skip pointer.
if (payloadUnitStartIndicator) {
int pointerField = tsBuffer.readBits(8);
@ -323,10 +324,10 @@ public final class TsExtractor {
int sectionLength = tsBuffer.readBits(12);
// Skip the rest of the PMT header.
tsBuffer.skipBits(60); // 16+2+5+1+8+8+3+13+4
int programInfoLength = tsBuffer.readBits(12);
// Read descriptors.
readDescriptors(tsBuffer, programInfoLength);
int programInfoLength = tsBuffer.readBits(12);
// Skip the descriptors.
tsBuffer.skipBytes(programInfoLength);
int entriesSize = sectionLength - 9 /* size of the rest of the fields before descriptors */
- programInfoLength - 4 /* CRC size */;
@ -335,9 +336,10 @@ public final class TsExtractor {
tsBuffer.skipBits(3);
int elementaryPid = tsBuffer.readBits(13);
tsBuffer.skipBits(4);
int esInfoLength = tsBuffer.readBits(12);
readDescriptors(tsBuffer, esInfoLength);
int esInfoLength = tsBuffer.readBits(12);
// Skip the descriptors.
tsBuffer.skipBytes(esInfoLength);
entriesSize -= esInfoLength + 5;
if (pesPayloadReaders.get(streamType) != null) {
@ -366,19 +368,6 @@ public final class TsExtractor {
// Skip CRC_32.
}
private void readDescriptors(BitsArray tsBuffer, int descriptorsSize) {
while (descriptorsSize > 0) {
// Skip tag.
tsBuffer.skipBits(8);
int descriptorsLength = tsBuffer.readBits(8);
if (descriptorsLength > 0) {
// Skip entire descriptor data.
tsBuffer.skipBytes(descriptorsLength);
}
descriptorsSize -= descriptorsSize + 2;
}
}
}
/**
@ -387,7 +376,7 @@ public final class TsExtractor {
private class PesReader extends TsPayloadReader {
// Reusable buffer for incomplete PES data.
private final BitsArray pesBuffer;
private final BitArray pesBuffer;
// Parses PES payload and extracts individual samples.
private final PesPayloadReader pesPayloadReader;
@ -396,11 +385,11 @@ public final class TsExtractor {
public PesReader(PesPayloadReader pesPayloadReader) {
this.pesPayloadReader = pesPayloadReader;
this.packetLength = -1;
pesBuffer = new BitsArray();
pesBuffer = new BitArray();
}
@Override
public void read(BitsArray tsBuffer, boolean payloadUnitStartIndicator) {
public void read(BitArray tsBuffer, boolean payloadUnitStartIndicator) {
if (payloadUnitStartIndicator && !pesBuffer.isEmpty()) {
// We've encountered the start of the next packet, but haven't yet read the body. Read it.
// Note that this should only happen if the packet length was unspecified.
@ -484,7 +473,7 @@ public final class TsExtractor {
*/
private abstract class PesPayloadReader {
public final Queue<Sample> sampleQueue;
public final LinkedList<Sample> sampleQueue;
private MediaFormat mediaFormat;
private boolean foundFirstKeyframe;
@ -506,7 +495,7 @@ public final class TsExtractor {
this.mediaFormat = mediaFormat;
}
public abstract void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs);
public abstract void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs);
public void clear() {
while (!sampleQueue.isEmpty()) {
@ -521,7 +510,7 @@ public final class TsExtractor {
* @param sampleSize The size of the sample data.
* @param sampleTimeUs The sample time stamp.
*/
protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, int flags) {
protected void addSample(BitArray buffer, int sampleSize, long sampleTimeUs, int flags) {
Sample sample = samplePool.get();
addToSample(sample, buffer, sampleSize);
sample.flags = flags;
@ -531,7 +520,7 @@ public final class TsExtractor {
@SuppressLint("InlinedApi")
protected void addSample(Sample sample) {
boolean isKeyframe = (sample.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
boolean isKeyframe = (sample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;
if (isKeyframe) {
if (!foundFirstKeyframe) {
foundFirstKeyframe = true;
@ -549,7 +538,7 @@ public final class TsExtractor {
}
}
protected void addToSample(Sample sample, BitsArray buffer, int size) {
protected void addToSample(Sample sample, BitArray buffer, int size) {
if (sample.data.length - sample.size < size) {
sample.expand(size - sample.data.length + sample.size);
}
@ -572,22 +561,24 @@ public final class TsExtractor {
*/
private class H264Reader extends PesPayloadReader {
// IDR picture.
private static final int NAL_UNIT_TYPE_IDR = 5;
// Access unit delimiter.
private static final int NAL_UNIT_TYPE_AUD = 9;
private static final int NAL_UNIT_TYPE_SPS = 7;
// Used to store uncompleted sample data.
private Sample currentSample;
public H264Reader() {
// TODO: Parse the format from the stream.
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
1920, 1080, null));
@Override
public void clear() {
super.clear();
if (currentSample != null) {
samplePool.recycle(currentSample);
currentSample = null;
}
}
@Override
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
// Read leftover frame data from previous PES packet.
pesPayloadSize -= readOneH264Frame(pesBuffer, true);
@ -597,6 +588,9 @@ public final class TsExtractor {
// Single PES packet should contain only one new H.264 frame.
if (currentSample != null) {
if (!hasMediaFormat() && (currentSample.flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
parseMediaFormat(currentSample);
}
addSample(currentSample);
}
currentSample = samplePool.get();
@ -609,32 +603,120 @@ public final class TsExtractor {
}
@SuppressLint("InlinedApi")
private int readOneH264Frame(BitsArray pesBuffer, boolean remainderOnly) {
private int readOneH264Frame(BitArray pesBuffer, boolean remainderOnly) {
int offset = remainderOnly ? 0 : 3;
int audStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_AUD, offset);
int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset);
if (audStart > 0) {
if (currentSample != null) {
addToSample(currentSample, pesBuffer, audStart);
if (idrStart < audStart) {
currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC;
}
} else {
pesBuffer.skipBytes(audStart);
if (currentSample != null) {
int idrStart = pesBuffer.findNextNalUnit(NAL_UNIT_TYPE_IDR, offset);
if (idrStart < audStart) {
currentSample.flags = MediaExtractor.SAMPLE_FLAG_SYNC;
}
return audStart;
addToSample(currentSample, pesBuffer, audStart);
} else {
pesBuffer.skipBytes(audStart);
}
return 0;
return audStart;
}
@Override
public void clear() {
super.clear();
if (currentSample != null) {
samplePool.recycle(currentSample);
currentSample = null;
private void parseMediaFormat(Sample sample) {
BitArray bitArray = new BitArray(sample.data, sample.size);
// Locate the SPS unit.
int spsOffset = bitArray.findNextNalUnit(NAL_UNIT_TYPE_SPS, 0);
if (spsOffset == bitArray.bytesLeft()) {
return;
}
int nextNalOffset = bitArray.findNextNalUnit(-1, spsOffset + 3);
// Unescape the SPS unit.
byte[] unescapedSps = unescapeStream(bitArray.getData(), spsOffset, nextNalOffset);
bitArray.reset(unescapedSps, unescapedSps.length);
// Parse the SPS unit
// Skip the NAL header.
bitArray.skipBytes(4);
// TODO: Handle different profiles properly.
bitArray.skipBytes(1);
// Skip 6 constraint bits, 2 reserved bits and level_idc.
bitArray.skipBytes(2);
// Skip seq_parameter_set_id.
bitArray.readExpGolombCodedInt();
// Skip log2_max_frame_num_minus4
bitArray.readExpGolombCodedInt();
long picOrderCntType = bitArray.readExpGolombCodedInt();
if (picOrderCntType == 0) {
// Skip log2_max_pic_order_cnt_lsb_minus4
bitArray.readExpGolombCodedInt();
} else if (picOrderCntType == 1) {
// Skip delta_pic_order_always_zero_flag
bitArray.skipBits(1);
// Skip offset_for_non_ref_pic (actually a signed value, but for skipping we can read it
// as though it were unsigned).
bitArray.readExpGolombCodedInt();
// Skip offset_for_top_to_bottom_field (actually a signed value, but for skipping we can
// read it as though it were unsigned).
bitArray.readExpGolombCodedInt();
long numRefFramesInPicOrderCntCycle = bitArray.readExpGolombCodedInt();
for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
// Skip offset_for_ref_frame[i]
bitArray.readExpGolombCodedInt();
}
}
// Skip max_num_ref_frames
bitArray.readExpGolombCodedInt();
// Skip gaps_in_frame_num_value_allowed_flag
bitArray.skipBits(1);
int picWidthInMbs = bitArray.readExpGolombCodedInt() + 1;
int picHeightInMapUnits = bitArray.readExpGolombCodedInt() + 1;
boolean frameMbsOnlyFlag = bitArray.readBit();
int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;
// Set the format.
setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
picWidthInMbs * 16, frameHeightInMbs * 16, null));
}
/**
* Replaces occurrences of [0, 0, 3] with [0, 0].
* <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 final BitsArray adtsBuffer;
private final BitArray adtsBuffer;
private long timeUs;
public AdtsReader() {
adtsBuffer = new BitsArray();
adtsBuffer = new BitArray();
}
@Override
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
boolean needToProcessLeftOvers = !adtsBuffer.isEmpty();
adtsBuffer.append(pesBuffer, pesPayloadSize);
// If there are leftovers from previous PES packet, process it with last calculated timeUs.
@ -751,7 +833,7 @@ public final class TsExtractor {
@SuppressLint("InlinedApi")
@Override
public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
public void read(BitArray pesBuffer, int pesPayloadSize, long pesTimeUs) {
addSample(pesBuffer, pesPayloadSize, pesTimeUs, MediaExtractor.SAMPLE_FLAG_SYNC);
}
@ -764,31 +846,33 @@ public final class TsExtractor {
private static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
private final ArrayList<Sample> samples;
private Sample firstInPool;
public SamplePool() {
samples = new ArrayList<Sample>();
}
/* package */ Sample get() {
if (samples.isEmpty()) {
/* package */ synchronized Sample get() {
if (firstInPool == null) {
return new Sample(DEFAULT_BUFFER_SEGMENT_SIZE);
}
return samples.remove(samples.size() - 1);
Sample sample = firstInPool;
firstInPool = sample.nextInPool;
sample.nextInPool = null;
return sample;
}
/* package */ void recycle(Sample sample) {
/* package */ synchronized void recycle(Sample sample) {
sample.reset();
samples.add(sample);
sample.nextInPool = firstInPool;
firstInPool = sample;
}
}
/**
* Simplified version of SampleHolder for internal buffering.
* An internal variant of {@link SampleHolder} for internal pooling and buffering.
*/
private static class Sample {
public Sample nextInPool;
public byte[] data;
public int flags;
public int size;

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

View File

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

View File

@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.parser.ts;
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.upstream.DataSource;
import java.io.IOException;
/**
* Wraps a byte array, providing methods that allow it to be read as a bitstream.
*/
public final class BitsArray {
public final class BitArray {
private byte[] data;
@ -33,16 +34,16 @@ public final class BitsArray {
private int byteOffset;
private int bitOffset;
public BitsArray() {
public BitArray() {
}
public BitsArray(byte[] data, int limit) {
public BitArray(byte[] data, int limit) {
this.data = data;
this.limit = limit;
}
/**
* Resets the state.
* Clears all data, setting the offset and limit to zero.
*/
public void reset() {
byteOffset = 0;
@ -50,6 +51,28 @@ public final class BitsArray {
limit = 0;
}
/**
* Resets to wrap the specified data, setting the offset to zero.
*
* @param data The data to wrap.
* @param limit The limit to set.
*/
public void reset(byte[] data, int limit) {
this.data = data;
this.limit = limit;
byteOffset = 0;
bitOffset = 0;
}
/**
* Gets the backing byte array.
*
* @return The backing byte array.
*/
public byte[] getData() {
return data;
}
/**
* Gets the current byte offset.
*
@ -69,16 +92,16 @@ public final class BitsArray {
}
/**
* Appends data from a {@link NonBlockingInputStream}.
* Appends data from a {@link DataSource}.
*
* @param inputStream The {@link NonBlockingInputStream} whose data should be appended.
* @param dataSource The {@link DataSource} from which to read.
* @param length The maximum number of bytes to read and append.
* @return The number of bytes that were read and appended. May be 0 if no data was available
* from the stream. -1 is returned if the end of the stream has been reached.
* @return The number of bytes that were read and appended, or -1 if no more data is available.
* @throws IOException If an error occurs reading from the source.
*/
public int append(NonBlockingInputStream inputStream, int length) {
public int append(DataSource dataSource, int length) throws IOException {
expand(length);
int bytesRead = inputStream.read(data, limit, length);
int bytesRead = dataSource.read(data, limit, length);
if (bytesRead == -1) {
return -1;
}
@ -87,12 +110,12 @@ public final class BitsArray {
}
/**
* Appends data from another {@link BitsArray}.
* Appends data from another {@link BitArray}.
*
* @param bitsArray The {@link BitsArray} whose data should be appended.
* @param bitsArray The {@link BitArray} whose data should be appended.
* @param length The number of bytes to read and append.
*/
public void append(BitsArray bitsArray, int length) {
public void append(BitArray bitsArray, int length) {
expand(length);
bitsArray.readBytes(data, limit, length);
limit += length;
@ -256,6 +279,19 @@ public final class BitsArray {
return limit == 0;
}
/**
* Reads an Exp-Golomb-coded format integer.
*
* @return The value of the parsed Exp-Golomb-coded integer.
*/
public int readExpGolombCodedInt() {
int leadingZeros = 0;
while (!readBit()) {
leadingZeros++;
}
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
}
/**
* Reads a Synchsafe integer.
* Synchsafe integers are integers that keep the highest bit of every byte zeroed.
@ -293,7 +329,7 @@ public final class BitsArray {
/**
* Finds the next NAL unit.
*
* @param nalUnitType The type of the NAL unit to search for.
* @param nalUnitType The type of the NAL unit to search for, or -1 for any NAL unit.
* @param offset The additional offset in the data to start the search from.
* @return The offset from the current position to the start of the NAL unit. If a NAL unit is
* not found, then the offset to the end of the data is returned.
@ -302,7 +338,7 @@ public final class BitsArray {
for (int i = byteOffset + offset; i < limit - 3; i++) {
// Check for NAL unit start code prefix == 0x000001.
if ((data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1)
&& (nalUnitType == (data[i + 3] & 0x1F))) {
&& (nalUnitType == -1 || (nalUnitType == (data[i + 3] & 0x1F)))) {
return i - byteOffset;
}
}