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; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer.demo;
import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.audio.AudioTrack; 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.demo.player.DemoPlayer;
import com.google.android.exoplayer.util.VerboseLogUtil; import com.google.android.exoplayer.util.VerboseLogUtil;
...@@ -91,11 +92,11 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -91,11 +92,11 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs, long length) { int mediaStartTimeMs, int mediaEndTimeMs) {
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime(); loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
if (VerboseLogUtil.isTagEnabled(TAG)) { if (VerboseLogUtil.isTagEnabled(TAG)) {
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
+ ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]"); + ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
} }
} }
...@@ -110,27 +111,22 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -110,27 +111,22 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) { public void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs) {
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
+ Integer.toString(trigger) + "]"); + Integer.toString(trigger) + "]");
} }
@Override @Override
public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) { public void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs) {
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
+ Integer.toString(trigger) + "]"); + Integer.toString(trigger) + "]");
} }
// DemoPlayer.InternalErrorListener // DemoPlayer.InternalErrorListener
@Override @Override
public void onUpstreamError(int sourceId, IOException e) { public void onLoadError(int sourceId, IOException e) {
printInternalError("upstreamError", e); printInternalError("loadError", e);
}
@Override
public void onConsumptionError(int sourceId, IOException e) {
printInternalError("consumptionError", e);
} }
@Override @Override
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource; 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.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
...@@ -113,8 +114,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -113,8 +114,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onAudioTrackWriteError(AudioTrack.WriteException e); void onAudioTrackWriteError(AudioTrack.WriteException e);
void onDecoderInitializationError(DecoderInitializationException e); void onDecoderInitializationError(DecoderInitializationException e);
void onCryptoError(CryptoException e); void onCryptoError(CryptoException e);
void onUpstreamError(int sourceId, IOException e); void onLoadError(int sourceId, IOException e);
void onConsumptionError(int sourceId, IOException e);
void onDrmSessionManagerError(Exception e); void onDrmSessionManagerError(Exception e);
} }
...@@ -122,12 +122,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -122,12 +122,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
* A listener for debugging information. * A listener for debugging information.
*/ */
public interface InfoListener { public interface InfoListener {
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs);
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs); void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs);
void onDroppedFrames(int count, long elapsed); void onDroppedFrames(int count, long elapsed);
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate); void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs, long length); int mediaStartTimeMs, int mediaEndTimeMs);
void onLoadCompleted(int sourceId, long bytesLoaded); void onLoadCompleted(int sourceId, long bytesLoaded);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs); long initializationDurationMs);
...@@ -432,15 +432,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -432,15 +432,14 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, public void onDownstreamFormatChanged(int sourceId, Format format, int trigger, int mediaTimeMs) {
int mediaTimeMs) {
if (infoListener == null) { if (infoListener == null) {
return; return;
} }
if (sourceId == TYPE_VIDEO) { if (sourceId == TYPE_VIDEO) {
infoListener.onVideoFormatEnabled(formatId, trigger, mediaTimeMs); infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
} else if (sourceId == TYPE_AUDIO) { } 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 ...@@ -490,16 +489,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onUpstreamError(int sourceId, IOException e) { public void onLoadError(int sourceId, IOException e) {
if (internalErrorListener != null) { if (internalErrorListener != null) {
internalErrorListener.onUpstreamError(sourceId, e); internalErrorListener.onLoadError(sourceId, e);
}
}
@Override
public void onConsumptionError(int sourceId, IOException e) {
if (internalErrorListener != null) {
internalErrorListener.onConsumptionError(sourceId, e);
} }
} }
...@@ -531,11 +523,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -531,11 +523,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
int mediaStartTimeMs, int mediaEndTimeMs, long length) { int mediaStartTimeMs, int mediaEndTimeMs) {
if (infoListener != null) { if (infoListener != null) {
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs, infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
mediaEndTimeMs, length); mediaEndTimeMs);
} }
} }
...@@ -552,14 +544,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -552,14 +544,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, 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) {
// Do nothing. // 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 @@ ...@@ -15,18 +15,10 @@
*/ */
package com.google.android.exoplayer.chunk; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSourceStream;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.TraceUtil;
import java.io.IOException;
/** /**
* An abstract base class for {@link Loadable} implementations that load chunks of data required * An abstract base class for {@link Loadable} implementations that load chunks of data required
...@@ -35,6 +27,31 @@ import java.io.IOException; ...@@ -35,6 +27,31 @@ import java.io.IOException;
public abstract class Chunk implements Loadable { 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. * Value of {@link #trigger} for a load whose reason is unspecified.
*/ */
public static final int TRIGGER_UNSPECIFIED = 0; public static final int TRIGGER_UNSPECIFIED = 0;
...@@ -56,20 +73,24 @@ public abstract class Chunk implements Loadable { ...@@ -56,20 +73,24 @@ public abstract class Chunk implements Loadable {
public static final int TRIGGER_CUSTOM_BASE = 10000; 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 int type;
public final Format format;
/** /**
* The reason for a {@link ChunkSource} having generated this chunk. For reporting only. Possible * The reason why the chunk was generated. For reporting only.
* values for this variable are defined by the specific {@link ChunkSource} implementations.
*/ */
public final int trigger; 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; protected final DataSource dataSource;
private final DataSpec dataSpec;
private DataSourceStream dataSourceStream;
/** /**
* @param dataSource The source from which the data should be loaded. * @param dataSource The source from which the data should be loaded.
...@@ -77,64 +98,16 @@ public abstract class Chunk implements Loadable { ...@@ -77,64 +98,16 @@ public abstract class Chunk implements Loadable {
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}. * {@link Integer#MAX_VALUE}.
* @param format See {@link #format}. * @param type See {@link #type}.
* @param trigger See {@link #trigger}. * @param trigger See {@link #trigger}.
* @param format See {@link #format}.
*/ */
public Chunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger) { public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = Assertions.checkNotNull(dataSource); this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec); this.dataSpec = Assertions.checkNotNull(dataSpec);
this.format = Assertions.checkNotNull(format); this.type = type;
this.trigger = trigger; this.trigger = trigger;
} this.format = format;
/**
* 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();
} }
/** /**
...@@ -142,65 +115,6 @@ public abstract class Chunk implements Loadable { ...@@ -142,65 +115,6 @@ public abstract class Chunk implements Loadable {
* *
* @return The number of bytes that have been loaded. * @return The number of bytes that have been loaded.
*/ */
public final long bytesLoaded() { public abstract 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();
}
}
} }
/*
* 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 { ...@@ -103,6 +103,14 @@ public interface ChunkSource {
IOException getError(); 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 * Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
* this source. * this source.
* *
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
...@@ -22,17 +22,16 @@ import java.io.IOException; ...@@ -22,17 +22,16 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
/** /**
* An abstract base class for {@link HlsChunk} implementations where the data should be loaded into * A base class for {@link Chunk} implementations where the data should be loaded into a
* a {@code byte[]} before being consumed. * {@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 static final int READ_GRANULARITY = 16 * 1024;
private byte[] data; private byte[] data;
private int limit; private int limit;
private volatile boolean loadFinished;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
/** /**
...@@ -41,36 +40,31 @@ public abstract class DataChunk extends HlsChunk { ...@@ -41,36 +40,31 @@ public abstract class DataChunk extends HlsChunk {
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed * the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}. * {@link Integer#MAX_VALUE}.
* @param 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. * @param data An optional recycled array that can be used as a holder for the data.
*/ */
public DataChunk(DataSource dataSource, DataSpec dataSpec, byte[] data) { public DataChunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format,
super(dataSource, dataSpec); byte[] data) {
super(dataSource, dataSpec, type, trigger, format);
this.data = data; this.data = data;
} }
@Override
public void consume() throws IOException {
consume(data, limit);
}
/** /**
* Invoked by {@link #consume()}. Implementations should override this method to consume the * Returns the array in which the data is held.
* loaded data. * <p>
* This method should be used for recycling the holder only, and not for reading the data.
* *
* @param data An array containing the data. * @return The array in which the data is held.
* @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; 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 @Override
public boolean isLoadFinished() { public long bytesLoaded() {
return loadFinished; return limit;
} }
// Loadable implementation // Loadable implementation
...@@ -98,12 +92,24 @@ public abstract class DataChunk extends HlsChunk { ...@@ -98,12 +92,24 @@ public abstract class DataChunk extends HlsChunk {
limit += bytesRead; limit += bytesRead;
} }
} }
loadFinished = !loadCanceled; if (!loadCanceled) {
consume(data, limit);
}
} finally { } finally {
dataSource.close(); 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() { private void maybeExpandData() {
if (data == null) { if (data == null) {
data = new byte[READ_GRANULARITY]; 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 @@ ...@@ -15,12 +15,9 @@
*/ */
package com.google.android.exoplayer.chunk; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; 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. * An abstract base class for {@link Chunk}s that contain media samples.
...@@ -36,103 +33,32 @@ public abstract class MediaChunk extends Chunk { ...@@ -36,103 +33,32 @@ public abstract class MediaChunk extends Chunk {
*/ */
public final long endTimeUs; 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 dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param trigger The reason for this chunk being selected. * @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 startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param 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, public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int nextChunkIndex) { long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec, format, trigger); super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format);
Assertions.checkNotNull(format);
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs; this.endTimeUs = endTimeUs;
this.nextChunkIndex = nextChunkIndex; this.chunkIndex = chunkIndex;
} this.isLastChunk = isLastChunk;
/**
* 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;
} }
/**
* 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 { ...@@ -107,6 +107,11 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
} }
@Override @Override
public void onChunkLoadCompleted(Chunk chunk) {
selectedSource.onChunkLoadCompleted(chunk);
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) { public void onChunkLoadError(Chunk chunk, Exception e) {
selectedSource.onChunkLoadError(chunk, e); selectedSource.onChunkLoadError(chunk, e);
} }
......
...@@ -98,13 +98,18 @@ public class SingleSampleChunkSource implements ChunkSource { ...@@ -98,13 +98,18 @@ public class SingleSampleChunkSource implements ChunkSource {
} }
@Override @Override
public void onChunkLoadCompleted(Chunk chunk) {
// Do nothing.
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) { public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing. // Do nothing.
} }
private SingleSampleMediaChunk initChunk() { private SingleSampleMediaChunk initChunk() {
return new SingleSampleMediaChunk(dataSource, dataSpec, format, 0, 0, durationUs, -1, return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_UNSPECIFIED, format, 0,
mediaFormat); durationUs, 0, true, mediaFormat, null, null);
} }
} }
...@@ -15,123 +15,113 @@ ...@@ -15,123 +15,113 @@
*/ */
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Assertions; 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 { public final class SingleSampleMediaChunk extends BaseMediaChunk {
/**
* The sample header data. May be null.
*/
public final byte[] headerData;
private final MediaFormat sampleFormat; private final MediaFormat sampleFormat;
private final DrmInitData sampleDrmInitData;
private final byte[] headerData;
/** private boolean writtenHeader;
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. private volatile int bytesLoaded;
* @param format The format of the stream to which this chunk belongs. private volatile boolean loadCanceled;
* @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);
}
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param trigger The reason for this chunk being selected. * @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 startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param chunkIndex The index of the chunk.
* @param sampleFormat The format of the media contained by 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 * @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 * prepended to the sample data. It is not reflected in the values returned by
* reflected in the values returned by {@link #bytesLoaded()} and {@link #getLength()}. * {@link #bytesLoaded()}.
*/ */
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat, Format format, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
byte[] headerData) { MediaFormat sampleFormat, DrmInitData sampleDrmInitData, byte[] headerData) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
true);
this.sampleFormat = sampleFormat; this.sampleFormat = sampleFormat;
this.sampleDrmInitData = sampleDrmInitData;
this.headerData = headerData; this.headerData = headerData;
} }
@Override @Override
public boolean prepare() { public long bytesLoaded() {
return true; return bytesLoaded;
} }
@Override @Override
public boolean sampleAvailable() { public MediaFormat getMediaFormat() {
return isLoadFinished() && !isReadFinished(); return sampleFormat;
} }
@Override @Override
public boolean read(SampleHolder holder) { public DrmInitData getDrmInitData() {
NonBlockingInputStream inputStream = getNonBlockingInputStream(); return sampleDrmInitData;
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;
} }
@Override // Loadable implementation.
public void seekToStart() {
resetReadPosition();
}
@Override @Override
public boolean seekTo(long positionUs, boolean allowNoop) { public void cancelLoad() {
resetReadPosition(); loadCanceled = true;
return true;
} }
@Override @Override
public MediaFormat getMediaFormat() { public boolean isLoadCanceled() {
return sampleFormat; return loadCanceled;
} }
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override @Override
public DrmInitData getDrmInitData() { public void load() throws IOException, InterruptedException {
return null; 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 @@ ...@@ -15,30 +15,25 @@
*/ */
package com.google.android.exoplayer.dash; 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.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. * media stream.
*/ */
public class DashWrappingSegmentIndex implements DashSegmentIndex { public class DashWrappingSegmentIndex implements DashSegmentIndex {
private final SegmentIndex segmentIndex; private final ChunkIndex chunkIndex;
private final String uri; 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 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) { public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) {
this.segmentIndex = segmentIndex; this.chunkIndex = chunkIndex;
this.uri = uri; this.uri = uri;
this.indexAnchor = indexAnchor;
} }
@Override @Override
...@@ -48,28 +43,27 @@ public class DashWrappingSegmentIndex implements DashSegmentIndex { ...@@ -48,28 +43,27 @@ public class DashWrappingSegmentIndex implements DashSegmentIndex {
@Override @Override
public int getLastSegmentNum() { public int getLastSegmentNum() {
return segmentIndex.length - 1; return chunkIndex.length - 1;
} }
@Override @Override
public long getTimeUs(int segmentNum) { public long getTimeUs(int segmentNum) {
return segmentIndex.timesUs[segmentNum]; return chunkIndex.timesUs[segmentNum];
} }
@Override @Override
public long getDurationUs(int segmentNum) { public long getDurationUs(int segmentNum) {
return segmentIndex.durationsUs[segmentNum]; return chunkIndex.durationsUs[segmentNum];
} }
@Override @Override
public RangedUri getSegmentUrl(int segmentNum) { public RangedUri getSegmentUrl(int segmentNum) {
return new RangedUri(uri, null, indexAnchor + segmentIndex.offsets[segmentNum], return new RangedUri(uri, null, chunkIndex.offsets[segmentNum], chunkIndex.sizes[segmentNum]);
segmentIndex.sizes[segmentNum]);
} }
@Override @Override
public int getSegmentNum(long timeUs) { public int getSegmentNum(long timeUs) {
return Util.binarySearchFloor(segmentIndex.timesUs, timeUs, true, true); return chunkIndex.getChunkIndex(timeUs);
} }
@Override @Override
......
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.extractor; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -41,8 +41,11 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -41,8 +41,11 @@ public final class DefaultTrackOutput implements TrackOutput {
private volatile long largestParsedTimestampUs; private volatile long largestParsedTimestampUs;
private volatile MediaFormat format; 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); sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
needKeyframe = true; needKeyframe = true;
lastReadTimeUs = Long.MIN_VALUE; lastReadTimeUs = Long.MIN_VALUE;
......
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.extractor; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer.extractor;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.SampleHolder; 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.upstream.DataSource;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -33,7 +33,7 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -33,7 +33,7 @@ import java.util.concurrent.LinkedBlockingDeque;
private static final int INITIAL_SCRATCH_SIZE = 32; private static final int INITIAL_SCRATCH_SIZE = 32;
private final BufferPool fragmentPool; private final Allocator allocator;
private final int fragmentLength; private final int fragmentLength;
private final InfoQueue infoQueue; private final InfoQueue infoQueue;
...@@ -49,9 +49,12 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -49,9 +49,12 @@ import java.util.concurrent.LinkedBlockingDeque;
private byte[] lastFragment; private byte[] lastFragment;
private int lastFragmentOffset; private int lastFragmentOffset;
public RollingSampleBuffer(BufferPool bufferPool) { /**
this.fragmentPool = bufferPool; * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
fragmentLength = bufferPool.bufferLength; */
public RollingSampleBuffer(Allocator allocator) {
this.allocator = allocator;
fragmentLength = allocator.getBufferLength();
infoQueue = new InfoQueue(); infoQueue = new InfoQueue();
dataQueue = new LinkedBlockingDeque<byte[]>(); dataQueue = new LinkedBlockingDeque<byte[]>();
extrasHolder = new SampleExtrasHolder(); extrasHolder = new SampleExtrasHolder();
...@@ -67,7 +70,7 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -67,7 +70,7 @@ import java.util.concurrent.LinkedBlockingDeque;
public void clear() { public void clear() {
infoQueue.clear(); infoQueue.clear();
while (!dataQueue.isEmpty()) { while (!dataQueue.isEmpty()) {
fragmentPool.releaseDirect(dataQueue.remove()); allocator.releaseBuffer(dataQueue.remove());
} }
totalBytesDropped = 0; totalBytesDropped = 0;
totalBytesWritten = 0; totalBytesWritten = 0;
...@@ -111,7 +114,7 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -111,7 +114,7 @@ import java.util.concurrent.LinkedBlockingDeque;
} }
// Discard the fragments. // Discard the fragments.
for (int i = 0; i < fragmentDiscardCount; i++) { for (int i = 0; i < fragmentDiscardCount; i++) {
fragmentPool.releaseDirect(dataQueue.removeLast()); allocator.releaseBuffer(dataQueue.removeLast());
} }
// Update lastFragment and lastFragmentOffset to reflect the new position. // Update lastFragment and lastFragmentOffset to reflect the new position.
lastFragment = dataQueue.peekLast(); lastFragment = dataQueue.peekLast();
...@@ -306,7 +309,7 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -306,7 +309,7 @@ import java.util.concurrent.LinkedBlockingDeque;
/** /**
* Discard any fragments that hold data prior to the specified absolute position, returning * 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. * @param absolutePosition The absolute position up to which fragments can be discarded.
*/ */
...@@ -314,7 +317,7 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -314,7 +317,7 @@ import java.util.concurrent.LinkedBlockingDeque;
int relativePosition = (int) (absolutePosition - totalBytesDropped); int relativePosition = (int) (absolutePosition - totalBytesDropped);
int fragmentIndex = relativePosition / fragmentLength; int fragmentIndex = relativePosition / fragmentLength;
for (int i = 0; i < fragmentIndex; i++) { for (int i = 0; i < fragmentIndex; i++) {
fragmentPool.releaseDirect(dataQueue.remove()); allocator.releaseBuffer(dataQueue.remove());
totalBytesDropped += fragmentLength; totalBytesDropped += fragmentLength;
} }
} }
...@@ -419,7 +422,7 @@ import java.util.concurrent.LinkedBlockingDeque; ...@@ -419,7 +422,7 @@ import java.util.concurrent.LinkedBlockingDeque;
private void ensureSpaceForWrite() { private void ensureSpaceForWrite() {
if (lastFragmentOffset == fragmentLength) { if (lastFragmentOffset == fragmentLength) {
lastFragmentOffset = 0; lastFragmentOffset = 0;
lastFragment = fragmentPool.allocateDirect(); lastFragment = allocator.allocateBuffer();
dataQueue.add(lastFragment); dataQueue.add(lastFragment);
} }
} }
......
...@@ -22,7 +22,7 @@ import java.util.ArrayList; ...@@ -22,7 +22,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public abstract class Atom { /* package*/ abstract class Atom {
/** Size of an atom header, in bytes. */ /** Size of an atom header, in bytes. */
public static final int HEADER_SIZE = 8; public static final int HEADER_SIZE = 8;
......
...@@ -31,7 +31,7 @@ import java.util.Collections; ...@@ -31,7 +31,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ /** 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.) */ /** 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}; private static final int[] AC3_CHANNEL_COUNTS = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
......
...@@ -15,8 +15,7 @@ ...@@ -15,8 +15,7 @@
*/ */
package com.google.android.exoplayer.extractor.mp4; package com.google.android.exoplayer.extractor.mp4;
// TODO: Make package private. /* package */ final class DefaultSampleValues {
public final class DefaultSampleValues {
public final int sampleDescriptionIndex; public final int sampleDescriptionIndex;
public final int duration; public final int duration;
......
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.mp4; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer.extractor.mp4;
/** /**
* Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream. * Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
*/ */
// TODO: Make package private.
public final class TrackEncryptionBox { public final class TrackEncryptionBox {
/** /**
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer.extractor.mp4; package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
...@@ -24,8 +23,7 @@ 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. * A holder for information corresponding to a single fragment of an mp4 file.
*/ */
// TODO: Make package private. /* package */ final class TrackFragment {
public final class TrackFragment {
public int sampleDescriptionIndex; public int sampleDescriptionIndex;
...@@ -147,22 +145,6 @@ public final class TrackFragment { ...@@ -147,22 +145,6 @@ public final class TrackFragment {
sampleEncryptionDataNeedsFill = false; 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) { public long getSamplePresentationTime(int index) {
return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
} }
......
...@@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions; ...@@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
/** Sample table for a track in an MP4 file. */ /** Sample table for a track in an MP4 file. */
public final class TrackSampleTable { /* package */ final class TrackSampleTable {
/** Sample index when no sample is available. */ /** Sample index when no sample is available. */
public static final int NO_SAMPLE = -1; public static final int NO_SAMPLE = -1;
......
...@@ -139,11 +139,10 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -139,11 +139,10 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
throws IOException { throws IOException {
ArrayList<Variant> variants = new ArrayList<Variant>(); ArrayList<Variant> variants = new ArrayList<Variant>();
ArrayList<Subtitle> subtitles = new ArrayList<Subtitle>(); ArrayList<Subtitle> subtitles = new ArrayList<Subtitle>();
int bandwidth = 0; int bitrate = 0;
String[] codecs = null; String codecs = null;
int width = -1; int width = -1;
int height = -1; int height = -1;
int variantIndex = 0;
boolean expectingStreamInfUrl = false; boolean expectingStreamInfUrl = false;
String line; String line;
...@@ -163,13 +162,8 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -163,13 +162,8 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
// TODO: Support other types of media tag. // TODO: Support other types of media tag.
} }
} else if (line.startsWith(STREAM_INF_TAG)) { } else if (line.startsWith(STREAM_INF_TAG)) {
bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX); codecs = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
if (codecsString != null) {
codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
} else {
codecs = null;
}
String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, String resolutionString = HlsParserUtil.parseOptionalStringAttr(line,
RESOLUTION_ATTR_REGEX); RESOLUTION_ATTR_REGEX);
if (resolutionString != null) { if (resolutionString != null) {
...@@ -182,8 +176,8 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli ...@@ -182,8 +176,8 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlayli
} }
expectingStreamInfUrl = true; expectingStreamInfUrl = true;
} else if (!line.startsWith("#") && expectingStreamInfUrl) { } else if (!line.startsWith("#") && expectingStreamInfUrl) {
variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height)); variants.add(new Variant(line, bitrate, codecs, width, height));
bandwidth = 0; bitrate = 0;
codecs = null; codecs = null;
width = -1; width = -1;
height = -1; height = -1;
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer.SampleHolder; ...@@ -22,6 +22,7 @@ import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
...@@ -62,7 +63,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -62,7 +63,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
private long pendingResetPositionUs; private long pendingResetPositionUs;
private TsChunk previousTsLoadable; private TsChunk previousTsLoadable;
private HlsChunk currentLoadable; private Chunk currentLoadable;
private boolean loadingFinished; private boolean loadingFinished;
private Loader loader; private Loader loader;
...@@ -284,23 +285,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -284,23 +285,15 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
try { chunkSource.onChunkLoadCompleted(currentLoadable);
currentLoadable.consume(); if (isTsChunk(currentLoadable)) {
} catch (IOException e) { TsChunk tsChunk = (TsChunk) loadable;
currentLoadableException = e; loadingFinished = tsChunk.isLastChunk;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
currentLoadableExceptionFatal = true;
} finally {
if (isTsChunk(currentLoadable)) {
TsChunk tsChunk = (TsChunk) loadable;
loadingFinished = tsChunk.isLastChunk;
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
maybeStartLoading();
} }
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
maybeStartLoading();
} }
@Override @Override
...@@ -314,7 +307,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -314,7 +307,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void onLoadError(Loadable loadable, IOException e) { public void onLoadError(Loadable loadable, IOException e) {
if (chunkSource.onLoadError(currentLoadable, e)) { if (chunkSource.onChunkLoadError(currentLoadable, e)) {
// Error handled by source. // Error handled by source.
clearCurrentLoadable(); clearCurrentLoadable();
} else { } else {
...@@ -417,7 +410,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -417,7 +410,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return; return;
} }
HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, Chunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
pendingResetPositionUs, downstreamPositionUs); pendingResetPositionUs, downstreamPositionUs);
if (nextLoadable == null) { if (nextLoadable == null) {
return; return;
...@@ -429,14 +422,14 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { ...@@ -429,14 +422,14 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (isPendingReset()) { if (isPendingReset()) {
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
} }
if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) { if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractorWrapper) {
extractors.addLast(previousTsLoadable.extractor); extractors.addLast(previousTsLoadable.extractorWrapper);
} }
} }
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
} }
private boolean isTsChunk(HlsChunk chunk) { private boolean isTsChunk(Chunk chunk) {
return chunk instanceof TsChunk; return chunk instanceof TsChunk;
} }
......
...@@ -15,77 +15,59 @@ ...@@ -15,77 +15,59 @@
*/ */
package com.google.android.exoplayer.hls; 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.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Util;
import java.io.IOException; 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; public final HlsExtractorWrapper extractorWrapper;
/**
* The start time of the media contained by the chunk. private final boolean isEncrypted;
*/
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;
private int loadPosition; private int bytesLoaded;
private volatile boolean loadFinished;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
* @param extractor An extractor to parse samples from the data. * @param trigger The reason for this chunk being selected.
* @param variantIndex The index of the variant in the master playlist. * @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 startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk. * @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise. * @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, public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
super(dataSource, dataSpec); HlsExtractorWrapper extractorWrapper, byte[] encryptionKey, byte[] encryptionIv) {
this.extractor = extractor; super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format,
this.variantIndex = variantIndex; startTimeUs, endTimeUs, chunkIndex, isLastChunk);
this.startTimeUs = startTimeUs; this.extractorWrapper = extractorWrapper;
this.endTimeUs = endTimeUs; // Note: this.dataSource and dataSource may be different.
this.chunkIndex = chunkIndex; this.isEncrypted = this.dataSource instanceof Aes128DataSource;
this.isLastChunk = isLastChunk;
} }
@Override @Override
public void consume() throws IOException { public long bytesLoaded() {
// Do nothing. return bytesLoaded;
}
@Override
public boolean isLoadFinished() {
return loadFinished;
} }
// Loadable implementation // Loadable implementation
...@@ -102,26 +84,51 @@ public final class TsChunk extends HlsChunk { ...@@ -102,26 +84,51 @@ public final class TsChunk extends HlsChunk {
@Override @Override
public void load() throws IOException, InterruptedException { 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 { try {
input = new DefaultExtractorInput(dataSource, 0, dataSource.open(dataSpec)); ExtractorInput input = new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition,
// If we previously fed part of this chunk to the extractor, skip it this time. dataSource.open(loadDataSpec));
// TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here, if (skipLoadedBytes) {
// rather than loading the whole chunk again and then skipping data we previously loaded. To input.skipFully(bytesLoaded);
// 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);
try { try {
int result = Extractor.RESULT_CONTINUE; int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input); result = extractorWrapper.read(input);
} }
} finally { } finally {
loadPosition = (int) input.getPosition(); bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
} }
} finally { } finally {
dataSource.close(); 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 @@ ...@@ -15,38 +15,19 @@
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.hls;
import java.util.Comparator;
/** /**
* Variant stream reference. * Variant stream reference.
*/ */
public final class Variant { public final class Variant {
/** public final int bitrate;
* Sorts {@link Variant} objects in order of decreasing bandwidth.
* <p>
* When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first.
*/
public static final class DecreasingBandwidthComparator implements Comparator<Variant> {
@Override
public int compare(Variant a, Variant b) {
int bandwidthDifference = b.bandwidth - a.bandwidth;
return bandwidthDifference != 0 ? bandwidthDifference : a.index - b.index;
}
}
public final int index;
public final int bandwidth;
public final String url; public final String url;
public final String[] codecs; public final String codecs;
public final int width; public final int width;
public final int height; public final int height;
public Variant(int index, String url, int bandwidth, String[] codecs, int width, int height) { public Variant(String url, int bitrate, String codecs, int width, int height) {
this.index = index; this.bitrate = bitrate;
this.bandwidth = bandwidth;
this.url = url; this.url = url;
this.codecs = codecs; this.codecs = codecs;
this.width = width; this.width = width;
......
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.chunk.Chunk; 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.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.ContainerMediaChunk; import com.google.android.exoplayer.chunk.ContainerMediaChunk;
...@@ -27,9 +28,8 @@ import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator; ...@@ -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;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk; 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.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.Track;
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
...@@ -70,7 +70,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -70,7 +70,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final int maxWidth; private final int maxWidth;
private final int maxHeight; 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 DrmInitData drmInitData;
private final SmoothStreamingFormat[] formats; private final SmoothStreamingFormat[] formats;
...@@ -152,7 +153,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -152,7 +153,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
formats = new SmoothStreamingFormat[trackCount]; formats = new SmoothStreamingFormat[trackCount];
extractors = new SparseArray<FragmentedMp4Extractor>(); extractorWrappers = new SparseArray<ChunkExtractorWrapper>();
mediaFormats = new SparseArray<MediaFormat>();
int maxWidth = 0; int maxWidth = 0;
int maxHeight = 0; int maxHeight = 0;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
...@@ -171,7 +173,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -171,7 +173,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale,
initialManifest.durationUs, mediaFormat, trackEncryptionBoxes)); initialManifest.durationUs, mediaFormat, trackEncryptionBoxes));
extractors.put(trackIndex, extractor); extractorWrappers.put(trackIndex, new ChunkExtractorWrapper(extractor));
mediaFormats.put(trackIndex, mediaFormat);
} }
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
this.maxWidth = maxWidth; this.maxWidth = maxWidth;
...@@ -271,7 +274,8 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -271,7 +274,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
chunkIndex = streamElement.getChunkIndex(seekPositionUs); chunkIndex = streamElement.getChunkIndex(seekPositionUs);
} else { } 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) { if (currentManifest.isLive) {
...@@ -295,14 +299,15 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -295,14 +299,15 @@ public class SmoothStreamingChunkSource implements ChunkSource {
boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1; boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1;
long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
long nextChunkStartTimeUs = isLastChunk ? -1 long chunkEndTimeUs = isLastChunk ? -1
: chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); : chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, chunkIndex); int trackIndex = selectedFormat.trackIndex;
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, Uri uri = streamElement.buildRequestUri(trackIndex, chunkIndex);
extractors.get(Integer.parseInt(selectedFormat.id)), drmInitData, dataSource, Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, extractorWrappers.get(trackIndex),
currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs, nextChunkStartTimeUs, 0); drmInitData, dataSource, currentAbsoluteChunkIndex, isLastChunk, chunkStartTimeUs,
chunkEndTimeUs, evaluation.trigger, mediaFormats.get(trackIndex));
out.chunk = mediaChunk; out.chunk = mediaChunk;
} }
...@@ -313,6 +318,11 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -313,6 +318,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
@Override @Override
public void onChunkLoadCompleted(Chunk chunk) {
// Do nothing.
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) { public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing. // Do nothing.
} }
...@@ -367,16 +377,16 @@ public class SmoothStreamingChunkSource implements ChunkSource { ...@@ -367,16 +377,16 @@ public class SmoothStreamingChunkSource implements ChunkSource {
} }
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
Extractor extractor, DrmInitData drmInitData, DataSource dataSource, int chunkIndex, ChunkExtractorWrapper extractorWrapper, DrmInitData drmInitData, DataSource dataSource,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) { int chunkIndex, boolean isLast, long chunkStartTimeUs, long chunkEndTimeUs,
int nextChunkIndex = isLast ? -1 : chunkIndex + 1; int trigger, MediaFormat mediaFormat) {
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
long offset = 0; long offset = 0;
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // 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. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
return new ContainerMediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs, return new ContainerMediaChunk(dataSource, dataSpec, trigger, formatInfo, chunkStartTimeUs,
nextStartTimeUs, nextChunkIndex, extractor, drmInitData, false, -chunkStartTimeUs); chunkEndTimeUs, chunkIndex, isLast, chunkStartTimeUs, extractorWrapper, mediaFormat,
drmInitData, true);
} }
private static byte[] getKeyId(byte[] initData) { 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 @@ ...@@ -16,32 +16,43 @@
package com.google.android.exoplayer.upstream; package com.google.android.exoplayer.upstream;
/** /**
* A source of {@link Allocation}s. * A source of allocations.
*/ */
public interface Allocator { 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 allocated buffer.
* @return The allocation.
*/ */
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 * 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 * has allocated, beyond the specified target number of bytes.
* number of bytes.
* *
* @param targetSize The target size in 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. * Returns the total size of all allocated buffers.
* */
* @return The number of allocated bytes. 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; ...@@ -20,23 +20,16 @@ import com.google.android.exoplayer.util.Assertions;
import java.util.Arrays; import java.util.Arrays;
/** /**
* An {@link Allocator} that maintains a pool of fixed length byte arrays (buffers). * Default implementation of {@link Allocator}.
* <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.
*/ */
public final class BufferPool implements Allocator { public final class BufferPool implements Allocator {
private static final int INITIAL_RECYCLED_BUFFERS_CAPACITY = 100; private static final int INITIAL_RECYCLED_BUFFERS_CAPACITY = 100;
/** private final int bufferLength;
* The length in bytes of each individual buffer in the pool.
*/
public final int bufferLength;
private int allocatedBufferCount; private int allocatedCount;
private int recycledBufferCount; private int recycledCount;
private byte[][] recycledBuffers; private byte[][] recycledBuffers;
/** /**
...@@ -51,81 +44,42 @@ public final class BufferPool implements Allocator { ...@@ -51,81 +44,42 @@ public final class BufferPool implements Allocator {
} }
@Override @Override
public synchronized int getAllocatedSize() { public synchronized byte[] allocateBuffer() {
return allocatedBufferCount * bufferLength; allocatedCount++;
return recycledCount > 0 ? recycledBuffers[--recycledCount] : new byte[bufferLength];
} }
@Override @Override
public synchronized void trim(int targetSize) { public synchronized void releaseBuffer(byte[] buffer) {
int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength; // Weak sanity check that the buffer probably originated from this pool.
int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedBufferCount); Assertions.checkArgument(buffer.length == bufferLength);
if (targetRecycledBufferCount < recycledBufferCount) { allocatedCount--;
Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledBufferCount, null); if (recycledCount == recycledBuffers.length) {
recycledBufferCount = targetRecycledBufferCount; recycledBuffers = Arrays.copyOf(recycledBuffers, recycledBuffers.length * 2);
} }
recycledBuffers[recycledCount++] = buffer;
// Wake up threads waiting for the allocated size to drop.
notifyAll();
} }
@Override @Override
public synchronized Allocation allocate(int size) { public synchronized void trim(int targetSize) {
return new AllocationImpl(allocate(size, null)); int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength;
} int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedCount);
if (targetRecycledBufferCount < recycledCount) {
/** Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledCount, null);
* Allocates byte arrays whose combined length is at least {@code size}. recycledCount = targetRecycledBufferCount;
* <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();
} }
return buffers;
} }
/** @Override
* Obtain a single buffer directly from the pool. public synchronized int getAllocatedSize() {
* <p> return allocatedCount * bufferLength;
* 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
* Return a single buffer to the pool. public int getBufferLength() {
* return bufferLength;
* @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;
} }
/** /**
...@@ -138,82 +92,4 @@ public final class BufferPool implements Allocator { ...@@ -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; ...@@ -24,7 +24,6 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* An HTTP specific extension to {@link DataSource}. * An HTTP specific extension to {@link DataSource}.
*/ */
...@@ -115,6 +114,15 @@ public interface HttpDataSource extends 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. * When the source is open, returns the url from which data is being read.
* <p> * <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; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -461,6 +462,25 @@ public final class Util { ...@@ -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} * 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. * as bytes. {@code string} must contain four or fewer characters.
*/ */
......
...@@ -60,43 +60,33 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -60,43 +60,33 @@ public class HlsMasterPlaylistParserTest extends TestCase {
assertNotNull(variants); assertNotNull(variants);
assertEquals(5, variants.size()); assertEquals(5, variants.size());
assertEquals(0, variants.get(0).index); assertEquals(1280000, variants.get(0).bitrate);
assertEquals(1280000, variants.get(0).bandwidth);
assertNotNull(variants.get(0).codecs); assertNotNull(variants.get(0).codecs);
assertEquals(2, variants.get(0).codecs.length); assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).codecs);
assertEquals("mp4a.40.2", variants.get(0).codecs[0]);
assertEquals("avc1.66.30", variants.get(0).codecs[1]);
assertEquals(304, variants.get(0).width); assertEquals(304, variants.get(0).width);
assertEquals(128, variants.get(0).height); assertEquals(128, variants.get(0).height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url); assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1, variants.get(1).index); assertEquals(1280000, variants.get(1).bitrate);
assertEquals(1280000, variants.get(1).bandwidth);
assertNotNull(variants.get(1).codecs); assertNotNull(variants.get(1).codecs);
assertEquals(2, variants.get(1).codecs.length); assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).codecs);
assertEquals("mp4a.40.2", variants.get(1).codecs[0]);
assertEquals("avc1.66.30", variants.get(1).codecs[1]);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url); assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2, variants.get(2).index); assertEquals(2560000, variants.get(2).bitrate);
assertEquals(2560000, variants.get(2).bandwidth);
assertEquals(null, variants.get(2).codecs); assertEquals(null, variants.get(2).codecs);
assertEquals(384, variants.get(2).width); assertEquals(384, variants.get(2).width);
assertEquals(160, variants.get(2).height); assertEquals(160, variants.get(2).height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url); assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(3, variants.get(3).index); assertEquals(7680000, variants.get(3).bitrate);
assertEquals(7680000, variants.get(3).bandwidth);
assertEquals(null, variants.get(3).codecs); assertEquals(null, variants.get(3).codecs);
assertEquals(-1, variants.get(3).width); assertEquals(-1, variants.get(3).width);
assertEquals(-1, variants.get(3).height); assertEquals(-1, variants.get(3).height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url); assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(4, variants.get(4).index); assertEquals(65000, variants.get(4).bitrate);
assertEquals(65000, variants.get(4).bandwidth);
assertNotNull(variants.get(4).codecs); assertNotNull(variants.get(4).codecs);
assertEquals(1, variants.get(4).codecs.length); assertEquals("mp4a.40.5", variants.get(4).codecs);
assertEquals("mp4a.40.5", variants.get(4).codecs[0]);
assertEquals(-1, variants.get(4).width); assertEquals(-1, variants.get(4).width);
assertEquals(-1, variants.get(4).height); assertEquals(-1, variants.get(4).height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); 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 @@ ...@@ -13,39 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer.hls; package com.google.android.exoplayer.util;
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;
/** /**
* An abstract base class for {@link Loadable} implementations that load chunks of data required * A {@link Clock} that returns a fixed value specified in the constructor.
* for the playback of HLS streams.
*/ */
public abstract class HlsChunk implements Loadable { public class FakeClock implements Clock {
protected final DataSource dataSource; private final long timeMs;
protected final DataSpec dataSpec;
/** public FakeClock(long timeMs) {
* @param dataSource The source from which the data should be loaded. this.timeMs = timeMs;
* @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 abstract void consume() throws IOException; @Override
public long elapsedRealtime() {
public abstract boolean isLoadFinished(); 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