Commit e21f7801 by Oliver Woodman

Major surgery to move all playback modes to the new Extractor model.

parent 265adf9a
Showing with 1092 additions and 1865 deletions
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer.demo;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.demo.player.DemoPlayer;
import com.google.android.exoplayer.util.VerboseLogUtil;
......@@ -91,11 +92,11 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@Override
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length) {
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs) {
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) {
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
+ ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
}
}
......@@ -110,27 +111,22 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@Override
public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", "
public void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs) {
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
+ Integer.toString(trigger) + "]");
}
@Override
public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", "
public void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs) {
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
+ Integer.toString(trigger) + "]");
}
// DemoPlayer.InternalErrorListener
@Override
public void onUpstreamError(int sourceId, IOException e) {
printInternalError("upstreamError", e);
}
@Override
public void onConsumptionError(int sourceId, IOException e) {
printInternalError("consumptionError", e);
public void onLoadError(int sourceId, IOException e) {
printInternalError("loadError", e);
}
@Override
......
......@@ -25,6 +25,7 @@ import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
......@@ -113,8 +114,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onAudioTrackWriteError(AudioTrack.WriteException e);
void onDecoderInitializationError(DecoderInitializationException e);
void onCryptoError(CryptoException e);
void onUpstreamError(int sourceId, IOException e);
void onConsumptionError(int sourceId, IOException e);
void onLoadError(int sourceId, IOException e);
void onDrmSessionManagerError(Exception e);
}
......@@ -122,12 +122,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
* A listener for debugging information.
*/
public interface InfoListener {
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs);
void onDroppedFrames(int count, long elapsed);
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length);
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs);
void onLoadCompleted(int sourceId, long bytesLoaded);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
......@@ -432,15 +432,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger,
int mediaTimeMs) {
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs) {
if (infoListener == null) {
return;
}
if (sourceId == TYPE_VIDEO) {
infoListener.onVideoFormatEnabled(formatId, trigger, mediaTimeMs);
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
} else if (sourceId == TYPE_AUDIO) {
infoListener.onAudioFormatEnabled(formatId, trigger, mediaTimeMs);
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
}
}
......@@ -490,16 +489,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onUpstreamError(int sourceId, IOException e) {
public void onLoadError(int sourceId, IOException e) {
if (internalErrorListener != null) {
internalErrorListener.onUpstreamError(sourceId, e);
}
}
@Override
public void onConsumptionError(int sourceId, IOException e) {
if (internalErrorListener != null) {
internalErrorListener.onConsumptionError(sourceId, e);
internalErrorListener.onLoadError(sourceId, e);
}
}
......@@ -531,11 +523,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length) {
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs) {
if (infoListener != null) {
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
mediaEndTimeMs, length);
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
mediaEndTimeMs);
}
}
......@@ -552,14 +544,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long bytesDiscarded) {
// Do nothing.
}
@Override
public void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long bytesDiscarded) {
public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs) {
// Do nothing.
}
......
/*
* 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.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.SampleSource;
import java.io.IOException;
/**
* Interface for callbacks to be notified of chunk based {@link SampleSource} events.
*/
public interface BaseChunkSampleSourceEventListener {
/**
* Invoked when an upstream load is started.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if
* the length of the data is not known in advance.
* @param type The type of the data being loaded.
* @param trigger The reason for the data being loaded.
* @param format The particular format to which this data corresponds, or null if the data being
* loaded does not correspond to a format.
* @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this
* load is for initialization data.
* @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
* load is for initialization data.
*/
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs);
/**
* Invoked when the current load operation completes.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param bytesLoaded The number of bytes that were loaded.
*/
void onLoadCompleted(int sourceId, long bytesLoaded);
/**
* Invoked when the current upstream load operation is canceled.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param bytesLoaded The number of bytes that were loaded prior to the cancellation.
*/
void onLoadCanceled(int sourceId, long bytesLoaded);
/**
* Invoked when an error occurs loading media data.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param e The cause of the failure.
*/
void onLoadError(int sourceId, IOException e);
/**
* Invoked when data is removed from the back of the buffer, typically so that it can be
* re-buffered using a different representation.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param mediaStartTimeMs The media time of the start of the discarded data.
* @param mediaEndTimeMs The media time of the end of the discarded data.
*/
void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs);
/**
* Invoked when the downstream format changes (i.e. when the format being supplied to the
* caller of {@link SampleSource#readData} changes).
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param format The format.
* @param trigger The trigger specified in the corresponding upstream load, as specified by the
* {@link ChunkSource}.
* @param mediaTimeMs The media time at which the change occurred.
*/
void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs);
}
/*
* 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.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.DefaultTrackOutput;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
/**
* A base implementation of {@link MediaChunk}, for chunks that contain a single track.
* <p>
* Loaded samples are output to a {@link DefaultTrackOutput}.
*/
public abstract class BaseMediaChunk extends MediaChunk {
/**
* Whether {@link #getMediaFormat()} and {@link #getDrmInitData()} can be called at any time to
* obtain the chunk's media format and drm initialization data. If false, these methods are only
* guaranteed to return correct data after the first sample data has been output from the chunk.
*/
public final boolean isFormatFinal;
private DefaultTrackOutput output;
private int firstSampleIndex;
/**
* @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 format The format of the stream to which this chunk belongs.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
* @param isFormatFinal True if {@link #getMediaFormat()} and {@link #getDrmInitData()} can be
* called at any time to obtain the media format and drm initialization data. False if these
* methods are only guaranteed to return correct data after the first sample data has been
* output from the chunk.
*/
public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
boolean isFormatFinal) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk);
this.isFormatFinal = isFormatFinal;
}
/**
* Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive
* samples as they are loaded.
*
* @param output The output that will receive the loaded samples.
*/
public void init(DefaultTrackOutput output) {
this.output = output;
this.firstSampleIndex = output.getWriteIndex();
}
/**
* Returns the index of the first sample in the output that was passed to
* {@link #init(DefaultTrackOutput)} that will originate from this chunk.
*/
public final int getFirstSampleIndex() {
return firstSampleIndex;
}
/**
* Gets the {@link MediaFormat} corresponding to the chunk.
* <p>
* See {@link #isFormatFinal} for information about when this method is guaranteed to return
* correct data.
*
* @return The {@link MediaFormat} corresponding to this chunk.
*/
public abstract MediaFormat getMediaFormat();
/**
* Gets the {@link DrmInitData} corresponding to the chunk.
* <p>
* See {@link #isFormatFinal} for information about when this method is guaranteed to return
* correct data.
*
* @return The {@link DrmInitData} corresponding to this chunk.
*/
public abstract DrmInitData getDrmInitData();
/**
* Returns the output most recently passed to {@link #init(DefaultTrackOutput)}.
*/
protected final DefaultTrackOutput getOutput() {
return output;
}
}
......@@ -15,18 +15,10 @@
*/
package com.google.android.exoplayer.chunk;
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 com.google.android.exoplayer.util.TraceUtil;
import java.io.IOException;
/**
* An abstract base class for {@link Loadable} implementations that load chunks of data required
......@@ -35,6 +27,31 @@ import java.io.IOException;
public abstract class Chunk implements Loadable {
/**
* Value of {@link #type} for chunks containing unspecified data.
*/
public static final int TYPE_UNSPECIFIED = 0;
/**
* Value of {@link #type} for chunks containing media data.
*/
public static final int TYPE_MEDIA = 1;
/**
* Value of {@link #type} for chunks containing media initialization data.
*/
public static final int TYPE_MEDIA_INITIALIZATION = 2;
/**
* Value of {@link #type} for chunks containing drm related data.
*/
public static final int TYPE_DRM = 3;
/**
* Value of {@link #type} for chunks containing manifest or playlist data.
*/
public static final int TYPE_MANIFEST = 4;
/**
* Implementations may define custom {@link #type} codes greater than or equal to this value.
*/
public static final int TYPE_CUSTOM_BASE = 10000;
/**
* Value of {@link #trigger} for a load whose reason is unspecified.
*/
public static final int TRIGGER_UNSPECIFIED = 0;
......@@ -56,20 +73,24 @@ public abstract class Chunk implements Loadable {
public static final int TRIGGER_CUSTOM_BASE = 10000;
/**
* The format associated with the data being loaded.
* The type of the chunk. For reporting only.
*/
// TODO: Consider removing this and pushing it down into MediaChunk instead.
public final Format format;
public final int type;
/**
* The reason for a {@link ChunkSource} having generated this chunk. For reporting only. Possible
* values for this variable are defined by the specific {@link ChunkSource} implementations.
* The reason why the chunk was generated. For reporting only.
*/
public final int trigger;
/**
* The format associated with the data being loaded, or null if the data being loaded is not
* associated with a specific format.
*/
public final Format format;
/**
* The {@link DataSpec} that defines the data to be loaded.
*/
public final DataSpec dataSpec;
private final DataSource dataSource;
private final DataSpec dataSpec;
private DataSourceStream dataSourceStream;
protected final DataSource dataSource;
/**
* @param dataSource The source from which the data should be loaded.
......@@ -77,64 +98,16 @@ public abstract class Chunk 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 format See {@link #format}.
* @param type See {@link #type}.
* @param trigger See {@link #trigger}.
* @param format See {@link #format}.
*/
public Chunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format) {
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
this.format = Assertions.checkNotNull(format);
this.type = type;
this.trigger = trigger;
}
/**
* Initializes the {@link Chunk}.
*
* @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);
}
/**
* Releases the {@link Chunk}, 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();
this.format = format;
}
/**
......@@ -142,65 +115,6 @@ public abstract class Chunk implements Loadable {
*
* @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 {
TraceUtil.beginSection("chunkLoad");
try {
dataSourceStream.load();
} finally {
TraceUtil.endSection();
}
}
public abstract long bytesLoaded();
}
/*
* 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.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
/**
* An {@link Extractor} wrapper for loading chunks containing a single track.
* <p>
* The wrapper allows switching of the {@link SingleTrackOutput} that receives parsed data.
*/
public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput {
/**
* Receives stream level data extracted by the wrapped {@link Extractor}.
*/
public interface SingleTrackOutput extends TrackOutput {
/**
* @see ExtractorOutput#seekMap(SeekMap)
*/
void seekMap(SeekMap seekMap);
/**
* @see ExtractorOutput#drmInitData(DrmInitData)
*/
void drmInitData(DrmInitData drmInitData);
}
private final Extractor extractor;
private boolean extractorInitialized;
private SingleTrackOutput output;
// Accessed only on the loader thread.
private boolean seenTrack;
/**
* @param extractor The extractor to wrap.
*/
public ChunkExtractorWrapper(Extractor extractor) {
this.extractor = extractor;
}
/**
* Initializes the extractor to output to the provided {@link SingleTrackOutput}, and configures
* it to receive data from a new chunk.
*
* @param output The {@link SingleTrackOutput} that will receive the parsed data.
*/
public void init(SingleTrackOutput output) {
this.output = output;
if (!extractorInitialized) {
extractor.init(this);
extractorInitialized = true;
} else {
extractor.seek();
}
}
/**
* Reads from the provided {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
* @throws IOException If an error occurred reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
public int read(ExtractorInput input) throws IOException, InterruptedException {
int result = extractor.read(input, null);
Assertions.checkState(result != Extractor.RESULT_SEEK);
return result;
}
// ExtractorOutput implementation.
@Override
public TrackOutput track(int id) {
Assertions.checkState(!seenTrack);
seenTrack = true;
return this;
}
@Override
public void endTracks() {
Assertions.checkState(seenTrack);
}
@Override
public void seekMap(SeekMap seekMap) {
output.seekMap(seekMap);
}
@Override
public void drmInitData(DrmInitData drmInitData) {
output.drmInitData(drmInitData);
}
// TrackOutput implementation.
@Override
public void format(MediaFormat format) {
output.format(format);
}
@Override
public int sampleData(ExtractorInput input, int length) throws IOException, InterruptedException {
return output.sampleData(input, length);
}
@Override
public void sampleData(ParsableByteArray data, int length) {
output.sampleData(data, length);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
output.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
}
}
......@@ -103,6 +103,14 @@ public interface ChunkSource {
IOException getError();
/**
* Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this
* source.
*
* @param chunk The chunk whose load has been completed.
*/
void onChunkLoadCompleted(Chunk chunk);
/**
* Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
* this source.
*
......
......@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
......@@ -22,17 +22,16 @@ import java.io.IOException;
import java.util.Arrays;
/**
* An abstract base class for {@link HlsChunk} implementations where the data should be loaded into
* a {@code byte[]} before being consumed.
* A base class for {@link Chunk} implementations where the data should be loaded into a
* {@code byte[]} before being consumed.
*/
public abstract class DataChunk extends HlsChunk {
public abstract class DataChunk extends Chunk {
private static final int READ_GRANULARITY = 16 * 1024;
private byte[] data;
private int limit;
private volatile boolean loadFinished;
private volatile boolean loadCanceled;
/**
......@@ -41,36 +40,31 @@ public abstract class DataChunk extends HlsChunk {
* {@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 type See {@link #type}.
* @param trigger See {@link #trigger}.
* @param format See {@link #format}.
* @param data An optional recycled array that can be used as a holder for the data.
*/
public DataChunk(DataSource dataSource, DataSpec dataSpec, byte[] data) {
super(dataSource, dataSpec);
public DataChunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format,
byte[] data) {
super(dataSource, dataSpec, type, trigger, format);
this.data = data;
}
@Override
public void consume() throws IOException {
consume(data, limit);
}
/**
* Invoked by {@link #consume()}. Implementations should override this method to consume the
* loaded data.
* Returns the array in which the data is held.
* <p>
* This method should be used for recycling the holder only, and not for reading the data.
*
* @param data An array containing the data.
* @param limit The limit of the data.
* @throws IOException If an error occurs consuming the loaded data.
* @return The array in which the data is held.
*/
protected abstract void consume(byte[] data, int limit) throws IOException;
public byte[] getDataHolder() {
return data;
}
/**
* 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;
public long bytesLoaded() {
return limit;
}
// Loadable implementation
......@@ -98,12 +92,24 @@ public abstract class DataChunk extends HlsChunk {
limit += bytesRead;
}
}
loadFinished = !loadCanceled;
if (!loadCanceled) {
consume(data, limit);
}
} finally {
dataSource.close();
}
}
/**
* Invoked by {@link #load()}. Implementations should override this method to consume the loaded
* data.
*
* @param data An array containing the data.
* @param limit The limit of the data.
* @throws IOException If an error occurs consuming the loaded data.
*/
protected abstract void consume(byte[] data, int limit) throws IOException;
private void maybeExpandData() {
if (data == null) {
data = new byte[READ_GRANULARITY];
......
/*
* 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.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper.SingleTrackOutput;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
/**
* A {@link Chunk} that uses an {@link Extractor} to parse initialization data for single track.
*/
public final class InitializationChunk extends Chunk implements SingleTrackOutput {
private final ChunkExtractorWrapper extractorWrapper;
// Initialization results. Set by the loader thread and read by any thread that knows loading
// has completed. These variables do not need to be volatile, since a memory barrier must occur
// for the reading thread to know that loading has completed.
private MediaFormat mediaFormat;
private DrmInitData drmInitData;
private SeekMap seekMap;
private volatile int bytesLoaded;
private volatile boolean loadCanceled;
/**
* Constructor for a chunk of media samples.
*
* @param dataSource A {@link DataSource} for loading the initialization data.
* @param dataSpec Defines the initialization data to be loaded.
* @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs.
* @param extractorWrapper A wrapped extractor to use for parsing the initialization data.
*/
public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
ChunkExtractorWrapper extractorWrapper) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA_INITIALIZATION, trigger, format);
this.extractorWrapper = extractorWrapper;
}
@Override
public long bytesLoaded() {
return bytesLoaded;
}
/**
* True if a {@link MediaFormat} was parsed from the chunk. False otherwise.
* <p>
* Should be called after loading has completed.
*/
public boolean hasFormat() {
return mediaFormat != null;
}
/**
* Returns a {@link MediaFormat} parsed from the chunk, or null.
* <p>
* Should be called after loading has completed.
*/
public MediaFormat getFormat() {
return mediaFormat;
}
/**
* True if a {@link DrmInitData} was parsed from the chunk. False otherwise.
* <p>
* Should be called after loading has completed.
*/
public boolean hasDrmInitData() {
return drmInitData != null;
}
/**
* Returns a {@link DrmInitData} parsed from the chunk, or null.
* <p>
* Should be called after loading has completed.
*/
public DrmInitData getDrmInitData() {
return drmInitData;
}
/**
* True if a {@link SeekMap} was parsed from the chunk. False otherwise.
* <p>
* Should be called after loading has completed.
*/
public boolean hasSeekMap() {
return seekMap != null;
}
/**
* Returns a {@link SeekMap} parsed from the chunk, or null.
* <p>
* Should be called after loading has completed.
*/
public SeekMap getSeekMap() {
return seekMap;
}
// SingleTrackOutput implementation.
@Override
public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap;
}
@Override
public void drmInitData(DrmInitData drmInitData) {
this.drmInitData = drmInitData;
}
@Override
public void format(MediaFormat mediaFormat) {
this.mediaFormat = mediaFormat;
}
@Override
public int sampleData(ExtractorInput input, int length) throws IOException, InterruptedException {
throw new IllegalStateException("Unexpected sample data in initialization chunk");
}
@Override
public void sampleData(ParsableByteArray data, int length) {
throw new IllegalStateException("Unexpected sample data in initialization chunk");
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
throw new IllegalStateException("Unexpected sample data in initialization chunk");
}
// Loadable implementation.
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
try {
// Create and open the input.
ExtractorInput input = new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition,
dataSource.open(loadDataSpec));
// Set the target to ourselves.
extractorWrapper.init(this);
// Load and parse the initialization data.
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractorWrapper.read(input);
}
} finally {
bytesLoaded += (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
dataSource.close();
}
}
}
......@@ -15,12 +15,9 @@
*/
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions;
/**
* An abstract base class for {@link Chunk}s that contain media samples.
......@@ -36,103 +33,32 @@ public abstract class MediaChunk extends Chunk {
*/
public final long endTimeUs;
/**
* The index of the next media chunk, or -1 if this is the last media chunk in the stream.
* The chunk index.
*/
public final int nextChunkIndex;
public final int chunkIndex;
/**
* True if this is the last chunk in the media. False otherwise.
*/
public final boolean isLastChunk;
/**
* Constructor for a chunk of media samples.
*
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs.
* @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 chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
*/
public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger,
long startTimeUs, long endTimeUs, int nextChunkIndex) {
super(dataSource, dataSpec, format, trigger);
public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format);
Assertions.checkNotNull(format);
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex;
}
/**
* Whether this is the last chunk in the stream.
*
* @return True if this is the last chunk in the stream. False otherwise.
*/
public final boolean isLastChunk() {
return nextChunkIndex == -1;
this.chunkIndex = chunkIndex;
this.isLastChunk = isLastChunk;
}
/**
* Seeks to the beginning of the chunk.
*/
public abstract void seekToStart();
/**
* Seeks to the specified position within the chunk.
*
* @param positionUs The desired seek time in microseconds.
* @param allowNoop True if the seek is allowed to do nothing if the result is more accurate than
* seeking to a key frame. Always pass false if it is required that the next sample be a key
* frame.
* @return True if the seek results in a discontinuity in the sequence of samples returned by
* {@link #read(SampleHolder)}. False otherwise.
*/
public abstract boolean seekTo(long positionUs, boolean allowNoop);
/**
* Prepares the chunk for reading. Does nothing if the chunk is already prepared.
* <p>
* Preparation may require consuming some of the chunk. If the data is not yet available then
* this method will return {@code false} rather than block. The method can be called repeatedly
* until the return value indicates success.
*
* @return True if the chunk was prepared. False otherwise.
* @throws ParserException If an error occurs parsing the media data.
*/
public abstract boolean prepare() throws ParserException;
/**
* Returns whether the next sample is available.
*
* @return True if the next sample is available for reading. False otherwise.
* @throws ParserException
*/
public abstract boolean sampleAvailable() throws ParserException;
/**
* Reads the next media sample from the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @param holder A holder to store the read sample.
* @return True if a sample was read. False if more data is still required.
* @throws ParserException If an error occurs parsing the media data.
* @throws IllegalStateException If called before {@link #init}, or after {@link #release}
*/
public abstract boolean read(SampleHolder holder) throws ParserException;
/**
* Returns the media format of the samples contained within this chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @return The sample media format.
*/
public abstract MediaFormat getMediaFormat();
/**
* Returns the DRM initialization data associated with the chunk.
* <p>
* Should only be called after the chunk has been successfully prepared.
*
* @return The DRM initialization data.
*/
public abstract DrmInitData getDrmInitData();
}
......@@ -107,6 +107,11 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
}
@Override
public void onChunkLoadCompleted(Chunk chunk) {
selectedSource.onChunkLoadCompleted(chunk);
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
selectedSource.onChunkLoadError(chunk, e);
}
......
......@@ -98,13 +98,18 @@ public class SingleSampleChunkSource implements ChunkSource {
}
@Override
public void onChunkLoadCompleted(Chunk chunk) {
// Do nothing.
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private SingleSampleMediaChunk initChunk() {
return new SingleSampleMediaChunk(dataSource, dataSpec, format, 0, 0, durationUs, -1,
mediaFormat);
return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_UNSPECIFIED, format, 0,
durationUs, 0, true, mediaFormat, null, null);
}
}
......@@ -15,123 +15,113 @@
*/
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
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.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
/**
* A {@link MediaChunk} containing a single sample.
* A {@link BaseMediaChunk} for chunks consisting of a single raw sample.
*/
public class SingleSampleMediaChunk extends MediaChunk {
/**
* The sample header data. May be null.
*/
public final byte[] headerData;
public final class SingleSampleMediaChunk extends BaseMediaChunk {
private final MediaFormat sampleFormat;
private final DrmInitData sampleDrmInitData;
private final byte[] headerData;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param trigger The reason for this chunk being selected.
* @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 sampleFormat The format of the media contained by the chunk.
*/
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat) {
this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
sampleFormat, null);
}
private boolean writtenHeader;
private volatile int bytesLoaded;
private volatile boolean loadCanceled;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs.
* @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 sampleFormat The format of the media contained by the chunk.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
* @param sampleFormat The format of the sample.
* @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm
* protected.
* @param headerData Custom header data for the sample. May be null. If set, the header data is
* prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is not
* reflected in the values returned by {@link #bytesLoaded()} and {@link #getLength()}.
* prepended to the sample data. It is not reflected in the values returned by
* {@link #bytesLoaded()}.
*/
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat,
byte[] headerData) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
MediaFormat sampleFormat, DrmInitData sampleDrmInitData, byte[] headerData) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
true);
this.sampleFormat = sampleFormat;
this.sampleDrmInitData = sampleDrmInitData;
this.headerData = headerData;
}
@Override
public boolean prepare() {
return true;
public long bytesLoaded() {
return bytesLoaded;
}
@Override
public boolean sampleAvailable() {
return isLoadFinished() && !isReadFinished();
public MediaFormat getMediaFormat() {
return sampleFormat;
}
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
if (!sampleAvailable()) {
return false;
}
int bytesLoaded = (int) bytesLoaded();
int sampleSize = bytesLoaded;
if (headerData != null) {
sampleSize += headerData.length;
}
if (holder.data == null || holder.data.capacity() < sampleSize) {
holder.replaceBuffer(sampleSize);
}
int bytesRead;
if (holder.data != null) {
if (headerData != null) {
holder.data.put(headerData);
}
bytesRead = inputStream.read(holder.data, bytesLoaded);
holder.size = sampleSize;
} else {
bytesRead = inputStream.skip(bytesLoaded);
holder.size = 0;
}
Assertions.checkState(bytesRead == bytesLoaded);
holder.timeUs = startTimeUs;
return true;
public DrmInitData getDrmInitData() {
return sampleDrmInitData;
}
@Override
public void seekToStart() {
resetReadPosition();
}
// Loadable implementation.
@Override
public boolean seekTo(long positionUs, boolean allowNoop) {
resetReadPosition();
return true;
public void cancelLoad() {
loadCanceled = true;
}
@Override
public MediaFormat getMediaFormat() {
return sampleFormat;
public boolean isLoadCanceled() {
return loadCanceled;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public DrmInitData getDrmInitData() {
return null;
public void load() throws IOException, InterruptedException {
if (!writtenHeader) {
if (headerData != null) {
getOutput().sampleData(new ParsableByteArray(headerData), headerData.length);
}
writtenHeader = true;
}
DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
try {
// Create and open the input.
dataSource.open(loadDataSpec);
// Load the sample data.
int result = 0;
while (result != C.RESULT_END_OF_INPUT) {
result = getOutput().sampleData(dataSource, Integer.MAX_VALUE);
if (result != C.RESULT_END_OF_INPUT) {
bytesLoaded += result;
}
}
int sampleSize = bytesLoaded;
if (headerData != null) {
sampleSize += headerData.length;
}
getOutput().sampleMetadata(startTimeUs, 0, sampleSize, 0, null);
} finally {
dataSource.close();
}
}
}
/*
* 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.chunk.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
/**
* Facilitates extraction of media samples from a container format.
*/
public interface Extractor {
/**
* An attempt to read from the input stream returned insufficient data.
*/
public static final int RESULT_NEED_MORE_DATA = 1;
/**
* The end of the input stream was reached.
*/
public static final int RESULT_END_OF_STREAM = 2;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getDrmInitData()}.
*/
public static final int RESULT_READ_INIT = 8;
/**
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
/**
* Returns the segment index parsed from the stream.
*
* @return The segment index, or null if a SIDX atom has yet to be parsed.
*/
public SegmentIndex getIndex();
/**
* Returns true if the offsets in the index returned by {@link #getIndex()} are relative to the
* first byte following the initialization data, or false if they are absolute (i.e. relative to
* the first byte of the stream).
*
* @return True if the offsets are relative to the first byte following the initialization data.
* False otherwise.
*/
public boolean hasRelativeIndexOffsets();
/**
* Returns the format of the samples contained within the media stream.
*
* @return The sample media format, or null if the format has yet to be parsed.
*/
public MediaFormat getFormat();
/**
* Returns DRM initialization data parsed from the stream.
*
* @return The DRM initialization data. May be null if the initialization data has yet to be
* parsed, or if the stream does not contain any DRM initialization data.
*/
public DrmInitData getDrmInitData();
/**
* Consumes data from a {@link NonBlockingInputStream}.
* <p>
* The read terminates if the end of the input stream is reached, if an attempt to read from the
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
* both the reason for termination and data that was parsed during the read.
*
* @param inputStream The input stream from which data should be read.
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
* @return One or more of the {@code RESULT_*} flags defined in this class.
* @throws ParserException If an error occurs parsing the media data.
*/
public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException;
/**
* Seeks to a position before or equal to the requested time.
*
* @param seekTimeUs The desired seek time in microseconds.
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
* fragment run, is equal to or greater than the time of the current sample, and if there
* does not exist a sync frame between these two times.
* @return True if the operation resulted in a change of state. False if it was a no-op.
*/
public boolean seekTo(long seekTimeUs, boolean allowNoop);
}
/*
* 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.chunk.parser;
/**
* Defines segments within a media stream.
*/
public final class SegmentIndex {
/**
* The size in bytes of the segment index as it exists in the stream.
*/
public final int sizeBytes;
/**
* The number of segments.
*/
public final int length;
/**
* The segment sizes, in bytes.
*/
public final int[] sizes;
/**
* The segment byte offsets.
*/
public final long[] offsets;
/**
* The segment durations, in microseconds.
*/
public final long[] durationsUs;
/**
* The start time of each segment, in microseconds.
*/
public final long[] timesUs;
/**
* @param sizeBytes The size in bytes of the segment index as it exists in the stream.
* @param sizes The segment sizes, in bytes.
* @param offsets The segment byte offsets.
* @param durationsUs The segment durations, in microseconds.
* @param timesUs The start time of each segment, in microseconds.
*/
public SegmentIndex(int sizeBytes, int[] sizes, long[] offsets, long[] durationsUs,
long[] timesUs) {
this.sizeBytes = sizeBytes;
this.length = sizes.length;
this.sizes = sizes;
this.offsets = offsets;
this.durationsUs = durationsUs;
this.timesUs = timesUs;
}
}
/*
* 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.chunk.parser.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.nio.ByteBuffer;
/**
* Defines EBML element IDs/types and reacts to events.
*/
/* package */ interface EbmlEventHandler {
/**
* Retrieves the type of an element ID.
*
* <p>If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped.
* Note that all children of a skipped master element are also skipped.
*
* @param id The integer ID of this element
* @return One of the {@code TYPE_} constants defined in this class
*/
public int getElementType(int id);
/**
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
*
* <p>Following events should be considered as taking place "within" this element until a
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it is possible for
* another master element of the same ID to be nested within itself.
*
* @param id The integer ID of this element
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's children
* @throws ParserException If a parsing error occurs.
*/
public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes,
long contentsSizeBytes) throws ParserException;
/**
* Called when a master element has finished reading in all of its children from the
* {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @throws ParserException If a parsing error occurs.
*/
public void onMasterElementEnd(int id) throws ParserException;
/**
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The integer value this element contains
* @throws ParserException If a parsing error occurs.
*/
public void onIntegerElement(int id, long value) throws ParserException;
/**
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The float value this element contains
* @throws ParserException If a parsing error occurs.
*/
public void onFloatElement(int id, double value) throws ParserException;
/**
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
*
* @param id The integer ID of this element
* @param value The string value this element contains
* @throws ParserException If a parsing error occurs.
*/
public void onStringElement(int id, String value) throws ParserException;
/**
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
*
* <p>The element header (containing element ID and content size) will already have been read.
* Subclasses must either read nothing and return {@code false}, or exactly read the entire
* contents of the element, which is {@code contentsSizeBytes} in length, and return {@code true}.
*
* <p>It's guaranteed that the full element contents will be immediately available from
* {@code inputStream}.
*
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a
* binary element:
* <ul>
* <li>{@link EbmlReader#readVarint(NonBlockingInputStream)}.
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, byte[], int)}.
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}.
* <li>{@link EbmlReader#skipBytes(NonBlockingInputStream, int)}.
* <li>{@link EbmlReader#getBytesRead()}.
* </ul>
*
* @param id The integer ID of this element
* @param elementOffsetBytes The byte offset where this element starts
* @param headerSizeBytes The byte length of this element's ID and size header
* @param contentsSizeBytes The byte length of this element's contents
* @param inputStream The {@link NonBlockingInputStream} from which this
* element's contents should be read
* @return True if the element was read. False otherwise.
* @throws ParserException If a parsing error occurs.
*/
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) throws ParserException;
}
/*
* 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.chunk.parser.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.nio.ByteBuffer;
/**
* Basic event-driven incremental EBML parser which needs an {@link EbmlEventHandler} to
* define IDs/types and react to events.
*
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
* It was originally designed for the Matroska container format. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
*/
/* package */ interface EbmlReader {
// Element Types
/** Undefined element. */
public static final int TYPE_UNKNOWN = 0;
/** Contains child elements. */
public static final int TYPE_MASTER = 1;
/** Unsigned integer value of up to 8 bytes. */
public static final int TYPE_UNSIGNED_INT = 2;
public static final int TYPE_STRING = 3;
public static final int TYPE_BINARY = 4;
/** IEEE floating point value of either 4 or 8 bytes. */
public static final int TYPE_FLOAT = 5;
// Return values for reading methods.
public static final int READ_RESULT_CONTINUE = 0;
public static final int READ_RESULT_NEED_MORE_DATA = 1;
public static final int READ_RESULT_END_OF_STREAM = 2;
public void setEventHandler(EbmlEventHandler eventHandler);
/**
* Reads from a {@link NonBlockingInputStream}, invoking an event callback if possible.
*
* @param inputStream The input stream from which data should be read
* @return One of the {@code RESULT_*} flags defined in this interface
* @throws ParserException If parsing fails.
*/
public int read(NonBlockingInputStream inputStream) throws ParserException;
/**
* The total number of bytes consumed by the reader since first created or last {@link #reset()}.
*/
public long getBytesRead();
/**
* Resets the entire state of the reader so that it will read a new EBML structure from scratch.
*
* <p>This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
* all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events.
*/
public void reset();
/**
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents
* of a binary element.
*
* @param inputStream The input stream from which data should be read
* @return The varint value at the current position of the contents of a binary element
*/
public long readVarint(NonBlockingInputStream inputStream);
/**
* Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
*
* @param inputStream The input stream from which data should be read
* @param byteBuffer The {@link ByteBuffer} to which data should be written
* @param totalBytes The fixed number of bytes to be read and written
*/
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
/**
* Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
*
* @param inputStream The input stream from which data should be read
* @param byteArray The byte array to which data should be written
* @param totalBytes The fixed number of bytes to be read and written
*/
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
/**
* Skips a fixed number of bytes from the contents of a binary element.
*
* @param inputStream The input stream from which data should be skipped
* @param totalBytes The fixed number of bytes to be skipped
*/
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
}
......@@ -15,30 +15,25 @@
*/
package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.util.Util;
import com.google.android.exoplayer.extractor.ChunkIndex;
/**
* An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a
* An implementation of {@link DashSegmentIndex} that wraps a {@link ChunkIndex} parsed from a
* media stream.
*/
public class DashWrappingSegmentIndex implements DashSegmentIndex {
private final SegmentIndex segmentIndex;
private final ChunkIndex chunkIndex;
private final String uri;
private final long indexAnchor;
/**
* @param segmentIndex The {@link SegmentIndex} to wrap.
* @param chunkIndex The {@link ChunkIndex} to wrap.
* @param uri The URI where the data is located.
* @param indexAnchor The index anchor point. This value is added to the byte offsets specified
* in the wrapped {@link SegmentIndex}.
*/
public DashWrappingSegmentIndex(SegmentIndex segmentIndex, String uri, long indexAnchor) {
this.segmentIndex = segmentIndex;
public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) {
this.chunkIndex = chunkIndex;
this.uri = uri;
this.indexAnchor = indexAnchor;
}
@Override
......@@ -48,28 +43,27 @@ public class DashWrappingSegmentIndex implements DashSegmentIndex {
@Override
public int getLastSegmentNum() {
return segmentIndex.length - 1;
return chunkIndex.length - 1;
}
@Override
public long getTimeUs(int segmentNum) {
return segmentIndex.timesUs[segmentNum];
return chunkIndex.timesUs[segmentNum];
}
@Override
public long getDurationUs(int segmentNum) {
return segmentIndex.durationsUs[segmentNum];
return chunkIndex.durationsUs[segmentNum];
}
@Override
public RangedUri getSegmentUrl(int segmentNum) {
return new RangedUri(uri, null, indexAnchor + segmentIndex.offsets[segmentNum],
segmentIndex.sizes[segmentNum]);
return new RangedUri(uri, null, chunkIndex.offsets[segmentNum], chunkIndex.sizes[segmentNum]);
}
@Override
public int getSegmentNum(long timeUs) {
return Util.binarySearchFloor(segmentIndex.timesUs, timeUs, true, true);
return chunkIndex.getChunkIndex(timeUs);
}
@Override
......
......@@ -17,7 +17,7 @@ package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray;
......@@ -41,8 +41,11 @@ public final class DefaultTrackOutput implements TrackOutput {
private volatile long largestParsedTimestampUs;
private volatile MediaFormat format;
public DefaultTrackOutput(BufferPool bufferPool) {
rollingBuffer = new RollingSampleBuffer(bufferPool);
/**
* @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
*/
public DefaultTrackOutput(Allocator allocator) {
rollingBuffer = new RollingSampleBuffer(allocator);
sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE;
......
......@@ -17,7 +17,7 @@ package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray;
......@@ -33,7 +33,7 @@ import java.util.concurrent.LinkedBlockingDeque;
private static final int INITIAL_SCRATCH_SIZE = 32;
private final BufferPool fragmentPool;
private final Allocator allocator;
private final int fragmentLength;
private final InfoQueue infoQueue;
......@@ -49,9 +49,12 @@ import java.util.concurrent.LinkedBlockingDeque;
private byte[] lastFragment;
private int lastFragmentOffset;
public RollingSampleBuffer(BufferPool bufferPool) {
this.fragmentPool = bufferPool;
fragmentLength = bufferPool.bufferLength;
/**
* @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
*/
public RollingSampleBuffer(Allocator allocator) {
this.allocator = allocator;
fragmentLength = allocator.getBufferLength();
infoQueue = new InfoQueue();
dataQueue = new LinkedBlockingDeque<byte[]>();
extrasHolder = new SampleExtrasHolder();
......@@ -67,7 +70,7 @@ import java.util.concurrent.LinkedBlockingDeque;
public void clear() {
infoQueue.clear();
while (!dataQueue.isEmpty()) {
fragmentPool.releaseDirect(dataQueue.remove());
allocator.releaseBuffer(dataQueue.remove());
}
totalBytesDropped = 0;
totalBytesWritten = 0;
......@@ -111,7 +114,7 @@ import java.util.concurrent.LinkedBlockingDeque;
}
// Discard the fragments.
for (int i = 0; i < fragmentDiscardCount; i++) {
fragmentPool.releaseDirect(dataQueue.removeLast());
allocator.releaseBuffer(dataQueue.removeLast());
}
// Update lastFragment and lastFragmentOffset to reflect the new position.
lastFragment = dataQueue.peekLast();
......@@ -306,7 +309,7 @@ import java.util.concurrent.LinkedBlockingDeque;
/**
* Discard any fragments that hold data prior to the specified absolute position, returning
* them to the pool.
* them to the allocator.
*
* @param absolutePosition The absolute position up to which fragments can be discarded.
*/
......@@ -314,7 +317,7 @@ import java.util.concurrent.LinkedBlockingDeque;
int relativePosition = (int) (absolutePosition - totalBytesDropped);
int fragmentIndex = relativePosition / fragmentLength;
for (int i = 0; i < fragmentIndex; i++) {
fragmentPool.releaseDirect(dataQueue.remove());
allocator.releaseBuffer(dataQueue.remove());
totalBytesDropped += fragmentLength;
}
}
......@@ -419,7 +422,7 @@ import java.util.concurrent.LinkedBlockingDeque;
private void ensureSpaceForWrite() {
if (lastFragmentOffset == fragmentLength) {
lastFragmentOffset = 0;
lastFragment = fragmentPool.allocateDirect();
lastFragment = allocator.allocateBuffer();
dataQueue.add(lastFragment);
}
}
......
......@@ -22,7 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public abstract class Atom {
/* package*/ abstract class Atom {
/** Size of an atom header, in bytes. */
public static final int HEADER_SIZE = 8;
......
......@@ -31,7 +31,7 @@ import java.util.Collections;
import java.util.List;
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
public final class AtomParsers {
/* package */ final class AtomParsers {
/** Channel counts for AC-3 audio, indexed by acmod. (See ETSI TS 102 366.) */
private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
......
......@@ -15,8 +15,7 @@
*/
package com.google.android.exoplayer.extractor.mp4;
// TODO: Make package private.
public final class DefaultSampleValues {
/* package */ final class DefaultSampleValues {
public final int sampleDescriptionIndex;
public final int duration;
......
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.mp4;
/**
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
*/
// TODO: Make package private.
public final class TrackEncryptionBox {
/**
......
......@@ -16,7 +16,6 @@
package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException;
......@@ -24,8 +23,7 @@ import java.io.IOException;
/**
* A holder for information corresponding to a single fragment of an mp4 file.
*/
// TODO: Make package private.
public final class TrackFragment {
/* package */ final class TrackFragment {
public int sampleDescriptionIndex;
......@@ -147,22 +145,6 @@ public final class TrackFragment {
sampleEncryptionDataNeedsFill = false;
}
/**
* Fills {@link #sampleEncryptionData} for the current run from the provided source.
*
* @param source A source from which to read the encryption data.
* @return True if the encryption data was filled. False if the source had insufficient data.
*/
public boolean fillEncryptionData(NonBlockingInputStream source) {
if (source.getAvailableByteCount() < sampleEncryptionDataLength) {
return false;
}
source.read(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
sampleEncryptionData.setPosition(0);
sampleEncryptionDataNeedsFill = false;
return true;
}
public long getSamplePresentationTime(int index) {
return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
}
......
......@@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
/** Sample table for a track in an MP4 file. */
public final class TrackSampleTable {
/* package */ final class TrackSampleTable {
/** Sample index when no sample is available. */
public static final int NO_SAMPLE = -1;
......
......@@ -139,11 +139,10 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
throws IOException {
ArrayList<Variant> variants = new ArrayList<Variant>();
ArrayList<Subtitle> subtitles = new ArrayList<Subtitle>();
int bandwidth = 0;
String[] codecs = null;
int bitrate = 0;
String codecs = null;
int width = -1;
int height = -1;
int variantIndex = 0;
boolean expectingStreamInfUrl = false;
String line;
......@@ -163,13 +162,8 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
// TODO: Support other types of media tag.
}
} else if (line.startsWith(STREAM_INF_TAG)) {
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
if (codecsString != null) {
codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
} else {
codecs = null;
}
bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
codecs = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
String resolutionString = HlsParserUtil.parseOptionalStringAttr(line,
RESOLUTION_ATTR_REGEX);
if (resolutionString != null) {
......@@ -182,8 +176,8 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
}
expectingStreamInfUrl = true;
} else if (!line.startsWith("#") && expectingStreamInfUrl) {
variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
bandwidth = 0;
variants.add(new Variant(line, bitrate, codecs, width, height));
bitrate = 0;
codecs = null;
width = -1;
height = -1;
......
......@@ -22,6 +22,7 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
......@@ -62,7 +63,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private long pendingResetPositionUs;
private TsChunk previousTsLoadable;
private HlsChunk currentLoadable;
private Chunk currentLoadable;
private boolean loadingFinished;
private Loader loader;
......@@ -284,23 +285,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
@Override
public void onLoadCompleted(Loadable loadable) {
try {
currentLoadable.consume();
} catch (IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
currentLoadableExceptionFatal = true;
} finally {
if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk;
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
maybeStartLoading();
chunkSource.onChunkLoadCompleted(currentLoadable);
if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk;
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
maybeStartLoading();
}
@Override
......@@ -314,7 +307,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
@Override
public void onLoadError(Loadable loadable, IOException e) {
if (chunkSource.onLoadError(currentLoadable, e)) {
if (chunkSource.onChunkLoadError(currentLoadable, e)) {
// Error handled by source.
clearCurrentLoadable();
} else {
......@@ -417,7 +410,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return;
}
HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
Chunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
pendingResetPositionUs, downstreamPositionUs);
if (nextLoadable == null) {
return;
......@@ -429,14 +422,14 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (isPendingReset()) {
pendingResetPositionUs = NO_RESET_PENDING;
}
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) {
extractors.addLast(previousTsLoadable.extractor);
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractorWrapper) {
extractors.addLast(previousTsLoadable.extractorWrapper);
}
}
loader.startLoading(currentLoadable, this);
}
private boolean isTsChunk(HlsChunk chunk) {
private boolean isTsChunk(Chunk chunk) {
return chunk instanceof TsChunk;
}
......
......@@ -15,77 +15,59 @@
*/
package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
/**
* A MPEG2TS chunk.
* An MPEG2TS chunk.
*/
public final class TsChunk extends HlsChunk {
public final class TsChunk extends MediaChunk {
/**
* The index of the variant in the master playlist.
* The wrapped extractor into which this chunk is being consumed.
*/
public final int variantIndex;
/**
* The start time of the media contained by the chunk.
*/
public final long startTimeUs;
/**
* The end time of the media contained by the chunk.
*/
public final long endTimeUs;
/**
* The chunk index.
*/
public final int chunkIndex;
/**
* True if this is the last chunk in the media. False otherwise.
*/
public final boolean isLastChunk;
/**
* The extractor into which this chunk is being consumed.
*/
public final HlsExtractorWrapper extractor;
public final HlsExtractorWrapper extractorWrapper;
private final boolean isEncrypted;
private int loadPosition;
private volatile boolean loadFinished;
private int bytesLoaded;
private volatile boolean loadCanceled;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param extractor An extractor to parse samples from the data.
* @param variantIndex The index of the variant in the master playlist.
* @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise.
* @param extractorWrapper A wrapped extractor to parse samples from the data.
* @param encryptionKey For AES encryption chunks, the encryption key.
* @param encryptionIv For AES encryption chunks, the encryption initialization vector.
*/
public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractorWrapper extractor,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec);
this.extractor = extractor;
this.variantIndex = variantIndex;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.chunkIndex = chunkIndex;
this.isLastChunk = isLastChunk;
public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
HlsExtractorWrapper extractorWrapper, byte[] encryptionKey, byte[] encryptionIv) {
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format,
startTimeUs, endTimeUs, chunkIndex, isLastChunk);
this.extractorWrapper = extractorWrapper;
// Note: this.dataSource and dataSource may be different.
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
}
@Override
public void consume() throws IOException {
// Do nothing.
}
@Override
public boolean isLoadFinished() {
return loadFinished;
public long bytesLoaded() {
return bytesLoaded;
}
// Loadable implementation
......@@ -102,26 +84,51 @@ public final class TsChunk extends HlsChunk {
@Override
public void load() throws IOException, InterruptedException {
ExtractorInput input;
// If we previously fed part of this chunk to the extractor, we need to skip it this time. For
// encrypted content we need to skip the data by reading it through the source, so as to ensure
// correct decryption of the remainder of the chunk. For clear content, we can request the
// remainder of the chunk directly.
DataSpec loadDataSpec;
boolean skipLoadedBytes;
if (isEncrypted) {
loadDataSpec = dataSpec;
skipLoadedBytes = bytesLoaded != 0;
} else {
loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
skipLoadedBytes = false;
}
try {
input = new DefaultExtractorInput(dataSource, 0, dataSource.open(dataSpec));
// If we previously fed part of this chunk to the extractor, skip it this time.
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
// rather than loading the whole chunk again and then skipping data we previously loaded. To
// do this is straightforward for non-encrypted content, but more complicated for content
// encrypted with AES, for which we'll need to modify the way that decryption is performed.
input.skipFully(loadPosition);
ExtractorInput input = new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition,
dataSource.open(loadDataSpec));
if (skipLoadedBytes) {
input.skipFully(bytesLoaded);
}
try {
int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input);
result = extractorWrapper.read(input);
}
} finally {
loadPosition = (int) input.getPosition();
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}
} finally {
dataSource.close();
}
}
// Private methods
/**
* If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in
* order to decrypt the loaded data. Else returns the original.
*/
private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey,
byte[] encryptionIv) {
if (encryptionKey == null || encryptionIv == null) {
return dataSource;
}
return new Aes128DataSource(dataSource, encryptionKey, encryptionIv);
}
}
......@@ -15,38 +15,19 @@
*/
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 int bitrate;
public final String url;
public final String[] codecs;
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;
public Variant(String url, int bitrate, String codecs, int width, int height) {
this.bitrate = bitrate;
this.url = url;
this.codecs = codecs;
this.width = width;
......
......@@ -19,6 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.ContainerMediaChunk;
......@@ -27,9 +28,8 @@ import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.extractor.mp4.Track;
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
......@@ -70,7 +70,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxWidth;
private final int maxHeight;
private final SparseArray<FragmentedMp4Extractor> extractors;
private final SparseArray<ChunkExtractorWrapper> extractorWrappers;
private final SparseArray<MediaFormat> mediaFormats;
private final DrmInitData drmInitData;
private final SmoothStreamingFormat[] formats;
......@@ -152,7 +153,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
formats = new SmoothStreamingFormat[trackCount];
extractors = new SparseArray<FragmentedMp4Extractor>();
extractorWrappers = new SparseArray<ChunkExtractorWrapper>();
mediaFormats = new SparseArray<MediaFormat>();
int maxWidth = 0;
int maxHeight = 0;
for (int i = 0; i < trackCount; i++) {
......@@ -171,7 +173,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale,
initialManifest.durationUs, mediaFormat, trackEncryptionBoxes));
extractors.put(trackIndex, extractor);
extractorWrappers.put(trackIndex, new ChunkExtractorWrapper(extractor));
mediaFormats.put(trackIndex, mediaFormat);
}
this.maxHeight = maxHeight;
this.maxWidth = maxWidth;
......@@ -271,7 +274,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
chunkIndex = streamElement.getChunkIndex(seekPositionUs);
} else {
chunkIndex = queue.get(out.queueSize - 1).nextChunkIndex - currentManifestChunkOffset;
MediaChunk previous = queue.get(out.queueSize - 1);
chunkIndex = previous.isLastChunk ? -1 : previous.chunkIndex + 1 - currentManifestChunkOffset;
}
if (currentManifest.isLive) {
......@@ -295,14 +299,15 @@ public class SmoothStreamingChunkSource implements ChunkSource {
boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1;
long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
long nextChunkStartTimeUs = isLastChunk ? -1
long chunkEndTimeUs = isLastChunk ? -1
: chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(Integer.parseInt(selectedFormat.id)), drmInitData, dataSource,
currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0);
int trackIndex = selectedFormat.trackIndex;
Uri uri = streamElement.buildRequestUri(trackIndex, chunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, extractorWrappers.get(trackIndex),
drmInitData, dataSource, currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs,
chunkEndTimeUs, evaluation.trigger, mediaFormats.get(trackIndex));
out.chunk = mediaChunk;
}
......@@ -313,6 +318,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
@Override
public void onChunkLoadCompleted(Chunk chunk) {
// Do nothing.
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
......@@ -367,16 +377,16 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
Extractor extractor, DrmInitData drmInitData, DataSource dataSource, int chunkIndex,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
ChunkExtractorWrapper extractorWrapper, DrmInitData drmInitData, DataSource dataSource,
int chunkIndex, boolean isLast, long chunkStartTimeUs, long chunkEndTimeUs,
int trigger, MediaFormat mediaFormat) {
long offset = 0;
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
nextStartTimeUs, nextChunkIndex, extractor, drmInitData, false, -chunkStartTimeUs);
return new ContainerMediaChunk(dataSource, dataSpec, trigger, formatInfo, chunkStartTimeUs,
chunkEndTimeUs, chunkIndex, isLast, chunkStartTimeUs, extractorWrapper, mediaFormat,
drmInitData, true);
}
private static byte[] getKeyId(byte[] initData) {
......
/*
* 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.upstream;
/**
* An {@link Allocation}, defined to consist of a set of fragments of underlying byte arrays.
* <p>
* The byte arrays in which the fragments are located are obtained by {@link #getBuffers}. For
* each, the offset and length of the fragment within the byte array are obtained using
* {@link #getFragmentOffset} and {@link #getFragmentLength} respectively.
*/
public interface Allocation {
/**
* Ensures the allocation has a capacity greater than or equal to the specified size in bytes.
* <p>
* If {@code size} is greater than the current capacity of the allocation, then it will grow
* to have a capacity of at least {@code size}. The allocation is grown by adding new fragments.
* Existing fragments remain unchanged, and any data that has been written to them will be
* preserved.
* <p>
* If {@code size} is less than or equal to the capacity of the allocation, then the call is a
* no-op.
*
* @param size The minimum required capacity, in bytes.
*/
public void ensureCapacity(int size);
/**
* Gets the capacity of the allocation, in bytes.
*
* @return The capacity of the allocation, in bytes.
*/
public int capacity();
/**
* Gets the buffers in which the fragments are allocated.
*
* @return The buffers in which the fragments are allocated.
*/
public byte[][] getBuffers();
/**
* The offset of the fragment in the buffer at the specified index.
*
* @param index The index of the buffer.
* @return The offset of the fragment in the buffer.
*/
public int getFragmentOffset(int index);
/**
* The length of the fragment in the buffer at the specified index.
*
* @param index The index of the buffer.
* @return The length of the fragment in the buffer.
*/
public int getFragmentLength(int index);
/**
* Releases the allocation.
*/
public void release();
}
......@@ -16,32 +16,43 @@
package com.google.android.exoplayer.upstream;
/**
* A source of {@link Allocation}s.
* A source of allocations.
*/
public interface Allocator {
/**
* Obtains an allocation of at least the specified size.
* Obtain a buffer from the allocator.
* <p>
* When the caller has finished with the buffer, it should be returned by calling
* {@link #releaseBuffer(byte[])}.
*
* @param size The size of the required allocation, in bytes.
* @return The allocation.
* @return The allocated buffer.
*/
public Allocation allocate(int size);
byte[] allocateBuffer();
/**
* Return a buffer to the allocator.
*
* @param buffer The buffer being returned.
*/
void releaseBuffer(byte[] buffer);
/**
* Hints to the {@link Allocator} that it should make a best effort to release any memory that it
* has allocated for the purpose of backing {@link Allocation}s, beyond the specified target
* number of bytes.
* has allocated, beyond the specified target number of bytes.
*
* @param targetSize The target size in bytes.
*/
public void trim(int targetSize);
void trim(int targetSize);
/**
* Returns the number of bytes currently allocated in the form of {@link Allocation}s.
*
* @return The number of allocated bytes.
* Returns the total size of all allocated buffers.
*/
int getAllocatedSize();
/**
* Returns the length of each buffer provided by the allocator.
*/
public int getAllocatedSize();
int getBufferLength();
}
......@@ -20,23 +20,16 @@ import com.google.android.exoplayer.util.Assertions;
import java.util.Arrays;
/**
* An {@link Allocator} that maintains a pool of fixed length byte arrays (buffers).
* <p>
* An {@link Allocation} obtained from a {@link BufferPool} consists of the whole number of these
* buffers. When an {@link Allocation} is released, the underlying buffers are returned to the pool
* for re-use.
* Default implementation of {@link Allocator}.
*/
public final class BufferPool implements Allocator {
private static final int INITIAL_RECYCLED_BUFFERS_CAPACITY = 100;
/**
* The length in bytes of each individual buffer in the pool.
*/
public final int bufferLength;
private final int bufferLength;
private int allocatedBufferCount;
private int recycledBufferCount;
private int allocatedCount;
private int recycledCount;
private byte[][] recycledBuffers;
/**
......@@ -51,81 +44,42 @@ public final class BufferPool implements Allocator {
}
@Override
public synchronized int getAllocatedSize() {
return allocatedBufferCount * bufferLength;
public synchronized byte[] allocateBuffer() {
allocatedCount++;
return recycledCount > 0 ? recycledBuffers[--recycledCount] : new byte[bufferLength];
}
@Override
public synchronized void trim(int targetSize) {
int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength;
int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedBufferCount);
if (targetRecycledBufferCount < recycledBufferCount) {
Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledBufferCount, null);
recycledBufferCount = targetRecycledBufferCount;
public synchronized void releaseBuffer(byte[] buffer) {
// Weak sanity check that the buffer probably originated from this pool.
Assertions.checkArgument(buffer.length == bufferLength);
allocatedCount--;
if (recycledCount == recycledBuffers.length) {
recycledBuffers = Arrays.copyOf(recycledBuffers, recycledBuffers.length * 2);
}
recycledBuffers[recycledCount++] = buffer;
// Wake up threads waiting for the allocated size to drop.
notifyAll();
}
@Override
public synchronized Allocation allocate(int size) {
return new AllocationImpl(allocate(size, null));
}
/**
* Allocates byte arrays whose combined length is at least {@code size}.
* <p>
* An existing array of byte arrays may be provided to form the start of the allocation.
*
* @param size The total size required, in bytes.
* @param existing Existing byte arrays to use as the start of the allocation. May be null.
* @return The allocated byte arrays.
*/
/* package */ synchronized byte[][] allocate(int size, byte[][] existing) {
int requiredBufferCount = requiredBufferCount(size);
if (existing != null && requiredBufferCount <= existing.length) {
// The existing buffers are sufficient.
return existing;
}
// We need to allocate additional buffers.
byte[][] buffers = new byte[requiredBufferCount][];
int firstNewBufferIndex = 0;
if (existing != null) {
firstNewBufferIndex = existing.length;
System.arraycopy(existing, 0, buffers, 0, firstNewBufferIndex);
}
// Allocate the new buffers
allocatedBufferCount += requiredBufferCount - firstNewBufferIndex;
for (int i = firstNewBufferIndex; i < requiredBufferCount; i++) {
// Use a recycled buffer if one is available. Else instantiate a new one.
buffers[i] = nextBuffer();
public synchronized void trim(int targetSize) {
int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength;
int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedCount);
if (targetRecycledBufferCount < recycledCount) {
Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledCount, null);
recycledCount = targetRecycledBufferCount;
}
return buffers;
}
/**
* Obtain a single buffer directly from the pool.
* <p>
* When the caller has finished with the buffer, it should be returned to the pool by calling
* {@link #releaseDirect(byte[])}.
*
* @return The allocated buffer.
*/
public synchronized byte[] allocateDirect() {
allocatedBufferCount++;
return nextBuffer();
@Override
public synchronized int getAllocatedSize() {
return allocatedCount * bufferLength;
}
/**
* Return a single buffer to the pool.
*
* @param buffer The buffer being returned.
*/
public synchronized void releaseDirect(byte[] buffer) {
// Weak sanity check that the buffer probably originated from this pool.
Assertions.checkArgument(buffer.length == bufferLength);
allocatedBufferCount--;
ensureRecycledBufferCapacity(recycledBufferCount + 1);
recycledBuffers[recycledBufferCount++] = buffer;
@Override
public int getBufferLength() {
return bufferLength;
}
/**
......@@ -138,82 +92,4 @@ public final class BufferPool implements Allocator {
}
}
/**
* Returns the buffers belonging to an allocation to the pool.
*
* @param allocation The allocation to return.
*/
/* package */ synchronized void release(AllocationImpl allocation) {
byte[][] buffers = allocation.getBuffers();
allocatedBufferCount -= buffers.length;
int newRecycledBufferCount = recycledBufferCount + buffers.length;
ensureRecycledBufferCapacity(newRecycledBufferCount);
System.arraycopy(buffers, 0, recycledBuffers, recycledBufferCount, buffers.length);
recycledBufferCount = newRecycledBufferCount;
}
private int requiredBufferCount(long size) {
return (int) ((size + bufferLength - 1) / bufferLength);
}
private byte[] nextBuffer() {
return recycledBufferCount > 0 ? recycledBuffers[--recycledBufferCount]
: new byte[bufferLength];
}
private void ensureRecycledBufferCapacity(int requiredCapacity) {
if (recycledBuffers.length < requiredCapacity) {
// Expand the capacity of the recycled buffers array.
byte[][] newRecycledBuffers = new byte[requiredCapacity * 2][];
if (recycledBufferCount > 0) {
System.arraycopy(recycledBuffers, 0, newRecycledBuffers, 0, recycledBufferCount);
}
recycledBuffers = newRecycledBuffers;
}
}
private class AllocationImpl implements Allocation {
private byte[][] buffers;
public AllocationImpl(byte[][] buffers) {
this.buffers = buffers;
}
@Override
public void ensureCapacity(int size) {
buffers = allocate(size, buffers);
}
@Override
public int capacity() {
return bufferLength * buffers.length;
}
@Override
public byte[][] getBuffers() {
return buffers;
}
@Override
public int getFragmentOffset(int index) {
return 0;
}
@Override
public int getFragmentLength(int index) {
return bufferLength;
}
@Override
public void release() {
if (buffers != null) {
BufferPool.this.release(this);
buffers = null;
}
}
}
}
/*
* 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.upstream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
/**
* Input stream with non-blocking reading/skipping that also stores read/skipped data in a buffer.
* Call {@link #mark} to discard any buffered data before the current reading position. Call
* {@link #returnToMark} to move the current reading position back to the marked position, which is
* initially the start of the input stream.
*/
public final class BufferedNonBlockingInputStream implements NonBlockingInputStream {
private final NonBlockingInputStream inputStream;
private final byte[] bufferedBytes;
private long inputStreamPosition;
private int readPosition;
private int writePosition;
/**
* Wraps the specified {@code nonBlockingInputStream} for buffered reading using a buffer of size
* {@code bufferSize} bytes.
*/
public BufferedNonBlockingInputStream(
NonBlockingInputStream nonBlockingInputStream, int bufferSize) {
inputStream = Assertions.checkNotNull(nonBlockingInputStream);
bufferedBytes = new byte[bufferSize];
}
@Override
public int skip(int length) {
return consumeStream(null, null, 0, length);
}
@Override
public int read(byte[] buffer, int offset, int length) {
return consumeStream(null, buffer, offset, length);
}
@Override
public int read(ByteBuffer buffer, int length) {
return consumeStream(buffer, null, 0, length);
}
@Override
public long getAvailableByteCount() {
// The amount that can be read from the input stream is limited by how much can be buffered.
return (writePosition - readPosition)
+ Math.min(inputStream.getAvailableByteCount(), bufferedBytes.length - writePosition);
}
@Override
public boolean isEndOfStream() {
return writePosition == readPosition && inputStream.isEndOfStream();
}
@Override
public void close() {
inputStream.close();
inputStreamPosition = -1;
}
/** Returns the current position in the stream. */
public long getReadPosition() {
return inputStreamPosition - (writePosition - readPosition);
}
/**
* Moves the mark to be at the current position. Any data before the current position is
* discarded. After calling this method, calling {@link #returnToMark} will move the reading
* position back to the mark position.
*/
public void mark() {
System.arraycopy(bufferedBytes, readPosition, bufferedBytes, 0, writePosition - readPosition);
writePosition -= readPosition;
readPosition = 0;
}
/** Moves the current position back to the mark position. */
public void returnToMark() {
readPosition = 0;
}
/**
* Reads or skips data from the input stream. If {@code byteBuffer} is non-{@code null}, reads
* {@code length} bytes into {@code byteBuffer} (other arguments are ignored). If
* {@code byteArray} is non-{@code null}, reads {@code length} bytes into {@code byteArray} at
* {@code offset} (other arguments are ignored). Otherwise, skips {@code length} bytes.
*
* @param byteBuffer {@link ByteBuffer} to read into, or {@code null} to read into
* {@code byteArray} or skip.
* @param byteArray Byte array to read into, or {@code null} to read into {@code byteBuffer} or
* skip.
* @param offset Offset in {@code byteArray} to write to, if it is non-{@code null}.
* @param length Number of bytes to read or skip.
* @return The number of bytes consumed, or -1 if nothing was consumed and the end of stream was
* reached.
*/
private int consumeStream(ByteBuffer byteBuffer, byte[] byteArray, int offset, int length) {
// If necessary, reduce length so that we do not need to write past the end of the array.
int pendingBytes = writePosition - readPosition;
length = Math.min(length, bufferedBytes.length - writePosition + pendingBytes);
// If reading past the end of buffered data, request more and populate the buffer.
int streamBytesRead = 0;
if (length - pendingBytes > 0) {
streamBytesRead = inputStream.read(bufferedBytes, writePosition, length - pendingBytes);
if (streamBytesRead > 0) {
inputStreamPosition += streamBytesRead;
writePosition += streamBytesRead;
pendingBytes += streamBytesRead;
}
}
// Signal the end of the stream if nothing more will be read.
if (streamBytesRead == -1 && pendingBytes == 0) {
return -1;
}
// Fill the buffer using buffered data if reading, or just skip otherwise.
length = Math.min(pendingBytes, length);
if (byteBuffer != null) {
byteBuffer.put(bufferedBytes, readPosition, length);
} else if (byteArray != null) {
System.arraycopy(bufferedBytes, readPosition, byteArray, offset, length);
}
readPosition += length;
return length;
}
}
/*
* 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.upstream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
/**
* An implementation of {@link NonBlockingInputStream} for reading data from a byte array.
*/
public final class ByteArrayNonBlockingInputStream implements NonBlockingInputStream {
private final byte[] data;
private int position;
public ByteArrayNonBlockingInputStream(byte[] data) {
this.data = Assertions.checkNotNull(data);
}
@Override
public int skip(int length) {
int skipLength = getReadLength(length);
position += skipLength;
return skipLength;
}
@Override
public int read(byte[] buffer, int offset, int length) {
if (isEndOfStream()) {
return -1;
}
int readLength = getReadLength(length);
System.arraycopy(data, position, buffer, offset, readLength);
position += readLength;
return readLength;
}
@Override
public int read(ByteBuffer buffer, int length) {
if (isEndOfStream()) {
return -1;
}
int readLength = getReadLength(length);
buffer.put(data, position, readLength);
position += readLength;
return readLength;
}
@Override
public long getAvailableByteCount() {
return data.length - position;
}
@Override
public boolean isEndOfStream() {
return position == data.length;
}
@Override
public void close() {
// Do nothing.
}
private int getReadLength(int requestedLength) {
return Math.min(requestedLength, data.length - position);
}
}
......@@ -24,7 +24,6 @@ import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* An HTTP specific extension to {@link DataSource}.
*/
......@@ -115,6 +114,15 @@ public interface HttpDataSource extends DataSource {
}
@Override
long open(DataSpec dataSpec) throws HttpDataSourceException;
@Override
void close() throws HttpDataSourceException;
@Override
int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException;
/**
* When the source is open, returns the url from which data is being read.
* <p>
......
/*
* 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.upstream;
import java.nio.ByteBuffer;
/**
* Represents a source of bytes that can be consumed by downstream components.
* <p>
* The read and skip methods are non-blocking, and hence return 0 (indicating that no data has
* been read) in the case that data is not yet available to be consumed.
*/
public interface NonBlockingInputStream {
/**
* Skips over and discards up to {@code length} bytes of data. This method may skip over some
* smaller number of bytes, possibly 0.
*
* @param length The maximum number of bytes to skip.
* @return The actual number of bytes skipped, or -1 if the end of the data is reached.
*/
int skip(int length);
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}. This method may read fewer bytes, possibly 0.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param length The maximum number of bytes to read.
* @return The actual number of bytes read, or -1 if the end of the data is reached.
*/
int read(byte[] buffer, int offset, int length);
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}. This method may
* read fewer bytes, possibly 0.
*
* @param buffer The buffer into which the read data should be stored.
* @param length The maximum number of bytes to read.
* @return The actual number of bytes read, or -1 if the end of the data is reached.
*/
int read(ByteBuffer buffer, int length);
/**
* Returns the number of bytes currently available for reading or skipping. Calls to the read()
* and skip() methods are guaranteed to be satisfied in full if they request less than or
* equal to the value returned.
*
* @return The number of bytes currently available.
*/
long getAvailableByteCount();
/**
* Whether the end of the data has been reached.
*
* @return True if the end of the data has been reached, false otherwise.
*/
boolean isEndOfStream();
/**
* Closes the input stream.
*/
void close();
}
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import android.text.TextUtils;
......@@ -461,6 +462,25 @@ public final class Util {
}
/**
* Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec}
* that represents the remainder of the data.
*
* @param dataSpec The original {@link DataSpec}.
* @param bytesLoaded The number of bytes already loaded.
* @return A {@link DataSpec} that represents the remainder of the data.
*/
public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) {
if (bytesLoaded == 0) {
return dataSpec;
} else {
long remainingLength = dataSpec.length == C.LENGTH_UNBOUNDED ? C.LENGTH_UNBOUNDED
: dataSpec.length - bytesLoaded;
return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength,
dataSpec.key, dataSpec.flags);
}
}
/**
* Returns the integer equal to the big-endian concatenation of the characters in {@code string}
* as bytes. {@code string} must contain four or fewer characters.
*/
......
......@@ -60,43 +60,33 @@ public class HlsMasterPlaylistParserTest extends TestCase {
assertNotNull(variants);
assertEquals(5, variants.size());
assertEquals(0, variants.get(0).index);
assertEquals(1280000, variants.get(0).bandwidth);
assertEquals(1280000, variants.get(0).bitrate);
assertNotNull(variants.get(0).codecs);
assertEquals(2, variants.get(0).codecs.length);
assertEquals("mp4a.40.2", variants.get(0).codecs[0]);
assertEquals("avc1.66.30", variants.get(0).codecs[1]);
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).codecs);
assertEquals(304, variants.get(0).width);
assertEquals(128, variants.get(0).height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1, variants.get(1).index);
assertEquals(1280000, variants.get(1).bandwidth);
assertEquals(1280000, variants.get(1).bitrate);
assertNotNull(variants.get(1).codecs);
assertEquals(2, variants.get(1).codecs.length);
assertEquals("mp4a.40.2", variants.get(1).codecs[0]);
assertEquals("avc1.66.30", variants.get(1).codecs[1]);
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).codecs);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2, variants.get(2).index);
assertEquals(2560000, variants.get(2).bandwidth);
assertEquals(2560000, variants.get(2).bitrate);
assertEquals(null, variants.get(2).codecs);
assertEquals(384, variants.get(2).width);
assertEquals(160, variants.get(2).height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(3, variants.get(3).index);
assertEquals(7680000, variants.get(3).bandwidth);
assertEquals(7680000, variants.get(3).bitrate);
assertEquals(null, variants.get(3).codecs);
assertEquals(-1, variants.get(3).width);
assertEquals(-1, variants.get(3).height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(4, variants.get(4).index);
assertEquals(65000, variants.get(4).bandwidth);
assertEquals(65000, variants.get(4).bitrate);
assertNotNull(variants.get(4).codecs);
assertEquals(1, variants.get(4).codecs.length);
assertEquals("mp4a.40.5", variants.get(4).codecs[0]);
assertEquals("mp4a.40.5", variants.get(4).codecs);
assertEquals(-1, variants.get(4).width);
assertEquals(-1, variants.get(4).height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
......
/*
* 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.upstream;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.android.exoplayer.SampleSource;
import junit.framework.TestCase;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.Arrays;
/**
* Tests for {@link BufferedNonBlockingInputStream}.
*/
public class BufferedNonBlockingInputStreamTest extends TestCase {
private static final int BUFFER_SIZE_BYTES = 16;
@Mock private NonBlockingInputStream mockInputStream;
private BufferedNonBlockingInputStream bufferedInputStream;
@Override
public void setUp() {
MockitoAnnotations.initMocks(this);
bufferedInputStream = new BufferedNonBlockingInputStream(mockInputStream, BUFFER_SIZE_BYTES);
}
public void testSkipClipsCountToBufferSizeWhenMarkSet() {
// When marking and skipping more than the buffer size
bufferedInputStream.mark();
bufferedInputStream.skip(BUFFER_SIZE_BYTES + 1);
// Then BUFFER_SIZE_BYTES are read.
verify(mockInputStream).read((byte[]) any(), eq(0), eq(BUFFER_SIZE_BYTES));
}
public void testSkipResetSkipUsesBufferedData() {
// Given a buffered input stream that has already read BUFFER_SIZE_BYTES
stubInputStreamForReadingBytes();
bufferedInputStream.mark();
bufferedInputStream.skip(BUFFER_SIZE_BYTES);
verify(mockInputStream).read((byte[]) any(), eq(0), eq(BUFFER_SIZE_BYTES));
// When resetting and reading the same amount, no extra data are read.
bufferedInputStream.returnToMark();
bufferedInputStream.skip(BUFFER_SIZE_BYTES);
verify(mockInputStream).read((byte[]) any(), eq(0), eq(BUFFER_SIZE_BYTES));
}
public void testReturnsEndOfStreamAfterBufferedData() {
// Given a buffered input stream that has read 1 byte (to end-of-stream) and has been reset
stubInputStreamForReadingBytes();
bufferedInputStream.mark();
bufferedInputStream.skip(1);
stubInputStreamForReadingEndOfStream();
bufferedInputStream.returnToMark();
// When skipping, first 1 byte is returned, then end-of-stream.
assertEquals(1, bufferedInputStream.skip(1));
assertEquals(SampleSource.END_OF_STREAM, bufferedInputStream.skip(1));
}
public void testReadAtOffset() {
// Given a mock input stream that provide non-zero data
stubInputStreamForReadingBytes();
// When reading a byte at offset 1
byte[] bytes = new byte[2];
bufferedInputStream.mark();
bufferedInputStream.read(bytes, 1, 1);
// Then only the second byte is set.
assertTrue(Arrays.equals(new byte[] {(byte) 0, (byte) 0xFF}, bytes));
}
public void testSkipAfterMark() {
// Given a mock input stream that provides non-zero data, with three bytes read
stubInputStreamForReadingBytes();
bufferedInputStream.skip(1);
bufferedInputStream.mark();
bufferedInputStream.skip(2);
bufferedInputStream.returnToMark();
// Then it is possible to skip one byte after the mark and read two bytes.
assertEquals(1, bufferedInputStream.skip(1));
assertEquals(2, bufferedInputStream.read(new byte[2], 0, 2));
verify(mockInputStream).read((byte[]) any(), eq(0), eq(1));
verify(mockInputStream).read((byte[]) any(), eq(0), eq(2));
verify(mockInputStream).read((byte[]) any(), eq(2), eq(1));
}
/** Stubs the input stream to read 0xFF for all requests. */
private void stubInputStreamForReadingBytes() {
when(mockInputStream.read((byte[]) any(), anyInt(), anyInt())).thenAnswer(
new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
byte[] bytes = (byte[]) invocation.getArguments()[0];
int offset = (int) invocation.getArguments()[1];
int length = (int) invocation.getArguments()[2];
for (int i = 0; i < length; i++) {
bytes[i + offset] = (byte) 0xFF;
}
return length;
}
});
when(mockInputStream.skip(anyInt())).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return (int) invocation.getArguments()[0];
}
});
}
/** Stubs the input stream to read end-of-stream for all requests. */
private void stubInputStreamForReadingEndOfStream() {
when(mockInputStream.read((byte[]) any(), anyInt(), anyInt()))
.thenReturn(SampleSource.END_OF_STREAM);
}
}
/*
* 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.upstream;
import com.google.android.exoplayer.testutil.Util;
import junit.framework.TestCase;
import java.io.IOException;
import java.util.Arrays;
/**
* Unit tests for {@link DataSourceStream}.
*/
public class DataSourceStreamTest extends TestCase {
private static final int DATA_LENGTH = 1024;
private static final int BUFFER_LENGTH = 128;
public void testGetLoadedData() throws IOException, InterruptedException {
byte[] testData = Util.buildTestData(DATA_LENGTH);
DataSource dataSource = new ByteArrayDataSource(testData);
DataSpec dataSpec = new DataSpec(null, 0, DATA_LENGTH, null);
DataSourceStream dataSourceStream = new DataSourceStream(dataSource, dataSpec,
new BufferPool(BUFFER_LENGTH));
dataSourceStream.load();
// Assert that the read and load positions are correct.
assertEquals(0, dataSourceStream.getReadPosition());
assertEquals(testData.length, dataSourceStream.getLoadPosition());
int halfTestDataLength = testData.length / 2;
byte[] readData = new byte[testData.length];
int bytesRead = dataSourceStream.read(readData, 0, halfTestDataLength);
// Assert that the read position is updated correctly.
assertEquals(halfTestDataLength, bytesRead);
assertEquals(halfTestDataLength, dataSourceStream.getReadPosition());
bytesRead += dataSourceStream.read(readData, bytesRead, testData.length - bytesRead);
// Assert that the read position was updated correctly.
assertEquals(testData.length, bytesRead);
assertEquals(testData.length, dataSourceStream.getReadPosition());
// Assert that the data read using the two read calls either side of getLoadedData is correct.
assertTrue(Arrays.equals(testData, readData));
}
}
......@@ -13,39 +13,22 @@
* 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.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import java.io.IOException;
package com.google.android.exoplayer.util;
/**
* An abstract base class for {@link Loadable} implementations that load chunks of data required
* for the playback of HLS streams.
* A {@link Clock} that returns a fixed value specified in the constructor.
*/
public abstract class HlsChunk implements Loadable {
public class FakeClock implements Clock {
protected final DataSource dataSource;
protected final DataSpec dataSpec;
private final long timeMs;
/**
* @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}.
*/
public HlsChunk(DataSource dataSource, DataSpec dataSpec) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
public FakeClock(long timeMs) {
this.timeMs = timeMs;
}
public abstract void consume() throws IOException;
public abstract boolean isLoadFinished();
@Override
public long elapsedRealtime() {
return timeMs;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment