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

This commit is contained in:
Oliver Woodman 2015-04-13 18:57:31 +01:00
parent 265adf9a8f
commit e21f7801b5
55 changed files with 1780 additions and 5999 deletions

View file

@ -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
} }
@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
} }
@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

View file

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

View file

@ -0,0 +1,92 @@
/*
* 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);
}

View file

@ -0,0 +1,108 @@
/*
* 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;
}
}

View file

@ -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
@ -34,6 +26,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.
*/ */
@ -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 {
* {@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 {
* *
* @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();
}
}
} }

View file

@ -0,0 +1,144 @@
/*
* 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);
}
}

View file

@ -23,6 +23,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.extractor.DefaultTrackOutput;
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;
@ -32,7 +33,6 @@ import android.os.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -45,94 +45,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
/** /**
* Interface definition for a callback to be notified of {@link ChunkSampleSource} events. * Interface definition for a callback to be notified of {@link ChunkSampleSource} events.
*/ */
public interface EventListener { public interface EventListener extends BaseChunkSampleSourceEventListener {}
/**
* Invoked when an upstream load is started.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param formatId The format id.
* @param trigger A trigger for the format selection, as specified by the {@link ChunkSource}.
* @param isInitialization Whether the load is for format initialization data.
* @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.
* @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if
* the length of the data has not yet been determined.
*/
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
int mediaStartTimeMs, int mediaEndTimeMs, long length);
/**
* 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 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.
* @param bytesDiscarded The length of the data being discarded in bytes.
*/
void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long bytesDiscarded);
/**
* 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 onUpstreamError(int sourceId, IOException e);
/**
* Invoked when an error occurs consuming loaded data.
*
* @param sourceId The id of the reporting {@link SampleSource}.
* @param e The cause of the failure.
*/
void onConsumptionError(int sourceId, IOException e);
/**
* Invoked when data is removed from the front of the buffer, typically due to a seek or
* because the data has been consumed.
*
* @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.
* @param bytesDiscarded The length of the data being discarded in bytes.
*/
void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
long bytesDiscarded);
/**
* 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 formatId The format id.
* @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, String formatId, int trigger, int mediaTimeMs);
}
/** /**
* The default minimum number of times to retry loading data prior to failing. * The default minimum number of times to retry loading data prior to failing.
@ -149,8 +62,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
private final LoadControl loadControl; private final LoadControl loadControl;
private final ChunkSource chunkSource; private final ChunkSource chunkSource;
private final ChunkOperationHolder currentLoadableHolder; private final ChunkOperationHolder currentLoadableHolder;
private final LinkedList<MediaChunk> mediaChunks; private final LinkedList<BaseMediaChunk> mediaChunks;
private final List<MediaChunk> readOnlyMediaChunks; private final List<BaseMediaChunk> readOnlyMediaChunks;
private final DefaultTrackOutput sampleQueue;
private final int bufferSizeContribution; private final int bufferSizeContribution;
private final boolean frameAccurateSeeking; private final boolean frameAccurateSeeking;
private final Handler eventHandler; private final Handler eventHandler;
@ -165,6 +79,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
private boolean pendingDiscontinuity; private boolean pendingDiscontinuity;
private Loader loader; private Loader loader;
private boolean loadingFinished;
private IOException currentLoadableException; private IOException currentLoadableException;
private boolean currentLoadableExceptionFatal; private boolean currentLoadableExceptionFatal;
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
@ -197,9 +112,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
this.eventSourceId = eventSourceId; this.eventSourceId = eventSourceId;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
currentLoadableHolder = new ChunkOperationHolder(); currentLoadableHolder = new ChunkOperationHolder();
mediaChunks = new LinkedList<MediaChunk>(); mediaChunks = new LinkedList<BaseMediaChunk>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
state = STATE_UNPREPARED; state = STATE_UNPREPARED;
pendingResetPositionUs = NO_RESET_PENDING;
} }
/** /**
@ -259,7 +176,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
clearMediaChunks(); sampleQueue.clear();
mediaChunks.clear();
clearCurrentLoadable(); clearCurrentLoadable();
loadControl.trimAllocator(); loadControl.trimAllocator();
} }
@ -273,23 +191,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
chunkSource.continueBuffering(positionUs); chunkSource.continueBuffering(positionUs);
updateLoadControl(); updateLoadControl();
boolean haveSamples = false; boolean haveSamples = !sampleQueue.isEmpty();
if (isPendingReset() || mediaChunks.isEmpty()) {
// No sample available.
} else if (sampleAvailableOrFinishedLastChunk(mediaChunks.getFirst())) {
// There's a sample available to be read from the current chunk.
haveSamples = true;
} else {
// It may be the case that the current chunk has been fully read but not yet discarded and
// that the next chunk has an available sample. Return true if so, otherwise false.
haveSamples = mediaChunks.size() > 1
&& sampleAvailableOrFinishedLastChunk(mediaChunks.get(1));
}
if (!haveSamples) { if (!haveSamples) {
maybeThrowLoadableException(); maybeThrowLoadableException();
} }
return haveSamples; return loadingFinished || haveSamples;
} }
@Override @Override
@ -297,6 +203,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
Assertions.checkState(track == 0); Assertions.checkState(track == 0);
downstreamPositionUs = positionUs;
if (pendingDiscontinuity) { if (pendingDiscontinuity) {
pendingDiscontinuity = false; pendingDiscontinuity = false;
@ -307,85 +214,79 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
return NOTHING_READ; return NOTHING_READ;
} }
downstreamPositionUs = positionUs;
if (isPendingReset()) { if (isPendingReset()) {
maybeThrowLoadableException(); maybeThrowLoadableException();
IOException chunkSourceException = chunkSource.getError();
if (chunkSourceException != null) {
throw chunkSourceException;
}
return NOTHING_READ; return NOTHING_READ;
} }
MediaChunk mediaChunk = mediaChunks.getFirst(); boolean haveSamples = !sampleQueue.isEmpty();
if (mediaChunk.isReadFinished()) { BaseMediaChunk currentChunk = mediaChunks.getFirst();
// We've read all of the samples from the current media chunk. while (haveSamples && mediaChunks.size() > 1
if (mediaChunks.size() > 1) { && mediaChunks.get(1).getFirstSampleIndex() == sampleQueue.getReadIndex()) {
discardDownstreamMediaChunk(); mediaChunks.removeFirst();
mediaChunk = mediaChunks.getFirst(); currentChunk = mediaChunks.getFirst();
mediaChunk.seekToStart(); }
return readData(track, positionUs, formatHolder, sampleHolder, false);
} else if (mediaChunk.isLastChunk()) { if (downstreamFormat == null || !downstreamFormat.equals(currentChunk.format)) {
notifyDownstreamFormatChanged(currentChunk.format, currentChunk.trigger,
currentChunk.startTimeUs);
downstreamFormat = currentChunk.format;
}
if (haveSamples || currentChunk.isFormatFinal) {
MediaFormat mediaFormat = currentChunk.getMediaFormat();
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = currentChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
}
if (!haveSamples) {
if (loadingFinished) {
return END_OF_STREAM; return END_OF_STREAM;
} }
IOException chunkSourceException = chunkSource.getError();
if (chunkSourceException != null) {
throw chunkSourceException;
}
return NOTHING_READ;
}
if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
mediaChunk.startTimeUs);
downstreamFormat = mediaChunk.format;
}
if (!mediaChunk.prepare()) {
if (currentLoadableException != null) {
throw currentLoadableException;
}
return NOTHING_READ;
}
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getDrmInitData();
downstreamMediaFormat = mediaFormat;
return FORMAT_READ;
}
if (mediaChunk.read(sampleHolder)) {
boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
onSampleRead(mediaChunk, sampleHolder);
return SAMPLE_READ;
} else {
maybeThrowLoadableException(); maybeThrowLoadableException();
return NOTHING_READ; return NOTHING_READ;
} }
if (sampleQueue.getSample(sampleHolder)) {
boolean decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0;
onSampleRead(currentChunk, sampleHolder);
return SAMPLE_READ;
}
maybeThrowLoadableException();
return NOTHING_READ;
} }
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
if (pendingResetPositionUs == positionUs) { if ((isPendingReset() ? pendingResetPositionUs : downstreamPositionUs) == positionUs) {
return; return;
} }
MediaChunk mediaChunk = getMediaChunk(positionUs); downstreamPositionUs = positionUs;
if (mediaChunk == null) { // If we're not pending a reset, see if we can seek within the sample queue.
restartFrom(positionUs); boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs);
pendingDiscontinuity = true; if (seekInsideBuffer) {
// We succeeded. All we need to do is discard any chunks that we've moved past.
boolean haveSamples = !sampleQueue.isEmpty();
while (haveSamples && mediaChunks.size() > 1
&& mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) {
mediaChunks.removeFirst();
}
} else { } else {
pendingDiscontinuity |= mediaChunk.seekTo(positionUs, mediaChunk == mediaChunks.getFirst()); // We failed, and need to restart.
discardDownstreamMediaChunks(mediaChunk); restartFrom(positionUs);
updateLoadControl();
} }
// Either way, we need to send a discontinuity to the downstream components.
pendingDiscontinuity = true;
} }
private void maybeThrowLoadableException() throws IOException { private void maybeThrowLoadableException() throws IOException {
@ -393,19 +294,12 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
|| currentLoadableExceptionCount > minLoadableRetryCount)) { || currentLoadableExceptionCount > minLoadableRetryCount)) {
throw currentLoadableException; throw currentLoadableException;
} }
} if (sampleQueue.isEmpty() && currentLoadableHolder.chunk == null) {
IOException chunkSourceException = chunkSource.getError();
private MediaChunk getMediaChunk(long positionUs) { if (chunkSourceException != null) {
Iterator<MediaChunk> mediaChunkIterator = mediaChunks.iterator(); throw chunkSourceException;
while (mediaChunkIterator.hasNext()) {
MediaChunk mediaChunk = mediaChunkIterator.next();
if (positionUs < mediaChunk.startTimeUs) {
return null;
} else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) {
return mediaChunk;
} }
} }
return null;
} }
@Override @Override
@ -413,22 +307,12 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_ENABLED);
if (isPendingReset()) { if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} } else if (loadingFinished) {
MediaChunk mediaChunk = mediaChunks.getLast();
Chunk currentLoadable = currentLoadableHolder.chunk;
if (currentLoadable != null && mediaChunk == currentLoadable) {
// Linearly interpolate partially-fetched chunk times.
long chunkLength = mediaChunk.getLength();
if (chunkLength != C.LENGTH_UNBOUNDED && chunkLength != 0) {
return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
mediaChunk.bytesLoaded()) / chunkLength;
} else {
return mediaChunk.startTimeUs;
}
} else if (mediaChunk.isLastChunk()) {
return TrackRenderer.END_OF_TRACK_US; return TrackRenderer.END_OF_TRACK_US;
} else { } else {
return mediaChunk.endTimeUs; long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs();
return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs
: largestParsedTimestampUs;
} }
} }
@ -445,38 +329,26 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
Chunk currentLoadable = currentLoadableHolder.chunk; Chunk currentLoadable = currentLoadableHolder.chunk;
chunkSource.onChunkLoadCompleted(currentLoadable);
notifyLoadCompleted(currentLoadable.bytesLoaded()); notifyLoadCompleted(currentLoadable.bytesLoaded());
try { if (isMediaChunk(currentLoadable)) {
currentLoadable.consume(); loadingFinished = ((BaseMediaChunk) currentLoadable).isLastChunk;
} catch (IOException e) {
currentLoadableException = e;
currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
currentLoadableExceptionFatal = true;
notifyConsumptionError(e);
} finally {
if (!isMediaChunk(currentLoadable)) {
currentLoadable.release();
}
if (!currentLoadableExceptionFatal) {
clearCurrentLoadable();
}
updateLoadControl();
} }
clearCurrentLoadable();
updateLoadControl();
} }
@Override @Override
public void onLoadCanceled(Loadable loadable) { public void onLoadCanceled(Loadable loadable) {
Chunk currentLoadable = currentLoadableHolder.chunk; Chunk currentLoadable = currentLoadableHolder.chunk;
notifyLoadCanceled(currentLoadable.bytesLoaded()); notifyLoadCanceled(currentLoadable.bytesLoaded());
if (!isMediaChunk(currentLoadable)) {
currentLoadable.release();
}
clearCurrentLoadable(); clearCurrentLoadable();
if (state == STATE_ENABLED) { if (state == STATE_ENABLED) {
restartFrom(pendingResetPositionUs); restartFrom(pendingResetPositionUs);
} else { } else {
clearMediaChunks(); sampleQueue.clear();
mediaChunks.clear();
clearCurrentLoadable();
loadControl.trimAllocator(); loadControl.trimAllocator();
} }
} }
@ -486,39 +358,41 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
currentLoadableException = e; currentLoadableException = e;
currentLoadableExceptionCount++; currentLoadableExceptionCount++;
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
notifyUpstreamError(e); notifyLoadError(e);
chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e); chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
updateLoadControl(); updateLoadControl();
} }
/** /**
* Called when a sample has been read from a {@link MediaChunk}. Can be used to perform any * Called when a sample has been read. Can be used to perform any modifications necessary before
* modifications necessary before the sample is returned. * the sample is returned.
* *
* @param mediaChunk The MediaChunk the sample was ready from. * @param mediaChunk The chunk from which the sample was obtained.
* @param sampleHolder The sample that has just been read. * @param sampleHolder Holds the read sample.
*/ */
protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) { protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) {
// no-op // Do nothing.
} }
private void restartFrom(long positionUs) { private void restartFrom(long positionUs) {
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
clearMediaChunks(); sampleQueue.clear();
mediaChunks.clear();
clearCurrentLoadable(); clearCurrentLoadable();
updateLoadControl(); updateLoadControl();
} }
} }
private void clearMediaChunks() {
discardDownstreamMediaChunks(null);
}
private void clearCurrentLoadable() { private void clearCurrentLoadable() {
currentLoadableHolder.chunk = null; currentLoadableHolder.chunk = null;
clearCurrentLoadableException();
}
private void clearCurrentLoadableException() {
currentLoadableException = null; currentLoadableException = null;
currentLoadableExceptionCount = 0; currentLoadableExceptionCount = 0;
currentLoadableExceptionFatal = false; currentLoadableExceptionFatal = false;
@ -581,8 +455,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (isPendingReset()) { if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} else { } else {
MediaChunk lastMediaChunk = mediaChunks.getLast(); BaseMediaChunk lastMediaChunk = mediaChunks.getLast();
return lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; return lastMediaChunk.isLastChunk ? -1 : lastMediaChunk.endTimeUs;
} }
} }
@ -606,7 +480,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
// Chunk was unchanged. Resume loading. // Chunk was unchanged. Resume loading.
loader.startLoading(backedOffChunk, this); loader.startLoading(backedOffChunk, this);
} else { } else {
backedOffChunk.release(); // Chunk was changed. Notify that the existing load was canceled.
notifyLoadCanceled(backedOffChunk.bytesLoaded());
// Start loading the replacement.
maybeStartLoading(); maybeStartLoading();
} }
return; return;
@ -621,7 +497,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
// The current loadable is the last media chunk. Remove it before we invoke the chunk source, // The current loadable is the last media chunk. Remove it before we invoke the chunk source,
// and add it back again afterwards. // and add it back again afterwards.
MediaChunk removedChunk = mediaChunks.removeLast(); BaseMediaChunk removedChunk = mediaChunks.removeLast();
Assertions.checkState(backedOffChunk == removedChunk); Assertions.checkState(backedOffChunk == removedChunk);
currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs, chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs,
@ -632,10 +508,12 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
// Chunk was unchanged. Resume loading. // Chunk was unchanged. Resume loading.
loader.startLoading(backedOffChunk, this); loader.startLoading(backedOffChunk, this);
} else { } else {
// Chunk was changed. Notify that the existing load was canceled.
notifyLoadCanceled(backedOffChunk.bytesLoaded());
// This call will remove and release at least one chunk from the end of mediaChunks. Since // This call will remove and release at least one chunk from the end of mediaChunks. Since
// the current loadable is the last media chunk, it is guaranteed to be removed. // the current loadable is the last media chunk, it is guaranteed to be removed.
discardUpstreamMediaChunks(currentLoadableHolder.queueSize); discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
clearCurrentLoadable(); clearCurrentLoadableException();
maybeStartLoading(); maybeStartLoading();
} }
} }
@ -646,55 +524,22 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
// Nothing to load. // Nothing to load.
return; return;
} }
currentLoadable.init(loadControl.getAllocator());
if (isMediaChunk(currentLoadable)) { if (isMediaChunk(currentLoadable)) {
MediaChunk mediaChunk = (MediaChunk) currentLoadable; BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable;
mediaChunk.init(sampleQueue);
mediaChunks.add(mediaChunk);
if (isPendingReset()) { if (isPendingReset()) {
mediaChunk.seekTo(pendingResetPositionUs, false);
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
} }
mediaChunks.add(mediaChunk); notifyLoadStarted(mediaChunk.dataSpec.length, mediaChunk.type, mediaChunk.trigger,
notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false, mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs);
mediaChunk.startTimeUs, mediaChunk.endTimeUs, mediaChunk.getLength());
} else { } else {
notifyLoadStarted(currentLoadable.format.id, currentLoadable.trigger, true, -1, -1, notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type,
currentLoadable.getLength()); currentLoadable.trigger, currentLoadable.format, -1, -1);
} }
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
} }
/**
* Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
* itself discarded. Null can be passed to discard all media chunks.
*
* @param untilChunk The first media chunk to keep, or null to discard all media chunks.
*/
private void discardDownstreamMediaChunks(MediaChunk untilChunk) {
if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) {
return;
}
long totalBytes = 0;
long startTimeUs = mediaChunks.getFirst().startTimeUs;
long endTimeUs = 0;
while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) {
MediaChunk removed = mediaChunks.removeFirst();
totalBytes += removed.bytesLoaded();
endTimeUs = removed.endTimeUs;
removed.release();
}
notifyDownstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
}
/**
* Discards the first downstream media chunk.
*/
private void discardDownstreamMediaChunk() {
MediaChunk removed = mediaChunks.removeFirst();
long totalBytes = removed.bytesLoaded();
removed.release();
notifyDownstreamDiscarded(removed.startTimeUs, removed.endTimeUs, totalBytes);
}
/** /**
* Discard upstream media chunks until the queue length is equal to the length specified. * Discard upstream media chunks until the queue length is equal to the length specified.
* *
@ -705,25 +550,22 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
if (mediaChunks.size() <= queueLength) { if (mediaChunks.size() <= queueLength) {
return false; return false;
} }
long totalBytes = 0;
long startTimeUs = 0; long startTimeUs = 0;
long endTimeUs = mediaChunks.getLast().endTimeUs; long endTimeUs = mediaChunks.getLast().endTimeUs;
BaseMediaChunk removed = null;
while (mediaChunks.size() > queueLength) { while (mediaChunks.size() > queueLength) {
MediaChunk removed = mediaChunks.removeLast(); removed = mediaChunks.removeLast();
totalBytes += removed.bytesLoaded();
startTimeUs = removed.startTimeUs; startTimeUs = removed.startTimeUs;
removed.release();
} }
notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes); sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
notifyUpstreamDiscarded(startTimeUs, endTimeUs);
return true; return true;
} }
private boolean sampleAvailableOrFinishedLastChunk(MediaChunk chunk) throws IOException {
return chunk.sampleAvailable() || (chunk.isLastChunk() && chunk.isReadFinished());
}
private boolean isMediaChunk(Chunk chunk) { private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof MediaChunk; return chunk instanceof BaseMediaChunk;
} }
private boolean isPendingReset() { private boolean isPendingReset() {
@ -738,15 +580,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
return (int) (timeUs / 1000); return (int) (timeUs / 1000);
} }
private void notifyLoadStarted(final String formatId, final int trigger, private void notifyLoadStarted(final long length, final int type, final int trigger,
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs, final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs) {
final long length) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization, eventListener.onLoadStarted(eventSourceId, length, type, trigger, format,
usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), length); usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs));
} }
}); });
} }
@ -774,65 +615,40 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
} }
} }
private void notifyUpstreamError(final IOException e) { private void notifyLoadError(final IOException e) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onUpstreamError(eventSourceId, e); eventListener.onLoadError(eventSourceId, e);
} }
}); });
} }
} }
private void notifyConsumptionError(final IOException e) { private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onConsumptionError(eventSourceId, e);
}
});
}
}
private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
final long totalBytes) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
usToMs(mediaEndTimeUs), totalBytes); usToMs(mediaEndTimeUs));
} }
}); });
} }
} }
private void notifyDownstreamFormatChanged(final String formatId, final int trigger, private void notifyDownstreamFormatChanged(final Format format, final int trigger,
final long positionUs) { final long positionUs) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger, eventListener.onDownstreamFormatChanged(eventSourceId, format, trigger,
usToMs(positionUs)); usToMs(positionUs));
} }
}); });
} }
} }
private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
final long bytesDiscarded) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
usToMs(mediaEndTimeUs), bytesDiscarded);
}
});
}
}
} }

View file

@ -102,6 +102,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.

View file

@ -16,126 +16,77 @@
package com.google.android.exoplayer.chunk; package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper.SingleTrackOutput;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.DefaultTrackOutput;
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.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 android.util.Log;
import java.io.IOException;
/** /**
* A {@link MediaChunk} extracted from a container. * A {@link BaseMediaChunk} that uses an {@link Extractor} to parse sample data.
*/ */
public final class ContainerMediaChunk extends MediaChunk { public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOutput {
private final Extractor extractor; private static final String TAG = "ContainerMediaChunk";
private final boolean maybeSelfContained;
private final ChunkExtractorWrapper extractorWrapper;
private final long sampleOffsetUs; private final long sampleOffsetUs;
private boolean prepared;
private MediaFormat mediaFormat; private MediaFormat mediaFormat;
private DrmInitData drmInitData; private DrmInitData drmInitData;
/** private volatile int bytesLoaded;
* @deprecated Use the other constructor, passing null as {@code psshInfo}. private volatile boolean loadCanceled;
*/
@Deprecated
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
extractor, null, maybeSelfContained, sampleOffsetUs);
}
/** /**
* @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 extractor The extractor that will be used to extract the samples. * @param isLastChunk True if this is the last chunk in the media. False otherwise.
* @param drmInitData DRM initialization data. May be null if DRM initialization data is present * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.
* within the stream, meaning it can be obtained directly from {@code extractor}, or if no * @param extractorWrapper A wrapped extractor to use for parsing the data.
* DRM initialization data is required. * @param mediaFormat The {@link MediaFormat} of the chunk, if known. May be null if the data is
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might * known to define its own format.
* contain a moov atom defining the media format of the chunk. This parameter can always be * @param drmInitData The {@link DrmInitData} for the chunk. Null if the media is not drm
* safely set to true. Setting to false where the chunk is known to not be self contained may * protected. May also be null if the data is known to define its own initialization data.
* improve startup latency. * @param isFormatFinal True if {@code mediaFormat} and {@code drmInitData} are known to be
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor. * correct and final. False if the data may define its own format or initialization data.
*/ */
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, long sampleOffsetUs,
DrmInitData drmInitData, boolean maybeSelfContained, long sampleOffsetUs) { ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, DrmInitData drmInitData,
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex); boolean isFormatFinal) {
this.extractor = extractor; super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
this.maybeSelfContained = maybeSelfContained; isFormatFinal);
this.extractorWrapper = extractorWrapper;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
this.mediaFormat = mediaFormat;
this.drmInitData = drmInitData; this.drmInitData = drmInitData;
} }
@Override @Override
public void seekToStart() { public long bytesLoaded() {
extractor.seekTo(0, false); return bytesLoaded;
resetReadPosition();
} }
@Override @Override
public boolean seekTo(long positionUs, boolean allowNoop) { public void init(DefaultTrackOutput output) {
long seekTimeUs = positionUs + sampleOffsetUs; super.init(output);
boolean isDiscontinuous = extractor.seekTo(seekTimeUs, allowNoop); extractorWrapper.init(this);
if (isDiscontinuous) {
resetReadPosition();
}
return isDiscontinuous;
}
@Override
public boolean prepare() throws ParserException {
if (!prepared) {
if (maybeSelfContained) {
// Read up to the first sample. Once we're there, we know that the extractor must have
// parsed a moov atom if the chunk contains one.
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, null);
prepared = (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
} else {
// We know there isn't a moov atom. The extractor must have parsed one from a separate
// initialization chunk.
prepared = true;
}
if (prepared) {
mediaFormat = extractor.getFormat();
DrmInitData extractorDrmInitData = extractor.getDrmInitData();
if (extractorDrmInitData != null) {
drmInitData = extractorDrmInitData;
}
}
}
return prepared;
}
@Override
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override
public boolean read(SampleHolder holder) throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder);
boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0;
if (sampleRead) {
holder.timeUs -= sampleOffsetUs;
}
return sampleRead;
} }
@Override @Override
@ -148,4 +99,72 @@ public final class ContainerMediaChunk extends MediaChunk {
return drmInitData; return drmInitData;
} }
// SingleTrackOutput implementation.
@Override
public void seekMap(SeekMap seekMap) {
Log.w(TAG, "Ignoring unexpected 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 {
return getOutput().sampleData(input, length);
}
@Override
public void sampleData(ParsableByteArray data, int length) {
getOutput().sampleData(data, length);
}
@Override
public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) {
getOutput().sampleMetadata(timeUs + sampleOffsetUs, flags, size, offset, encryptionKey);
}
// 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();
}
}
} }

View file

@ -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;
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 {
* {@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 { * Returns the array in which the data is held.
consume(data, limit); * <p>
* This method should be used for recycling the holder only, and not for reading the data.
*
* @return The array in which the data is held.
*/
public byte[] getDataHolder() {
return data;
} }
/**
* Invoked by {@link #consume()}. 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;
/**
* 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 {
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];

View file

@ -0,0 +1,191 @@
/*
* 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();
}
}
}

View file

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

View file

@ -106,6 +106,11 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
} }
} }
@Override
public void onChunkLoadCompleted(Chunk chunk) {
selectedSource.onChunkLoadCompleted(chunk);
}
@Override @Override
public void onChunkLoadError(Chunk chunk, Exception e) { public void onChunkLoadError(Chunk chunk, Exception e) {
selectedSource.onChunkLoadError(chunk, e); selectedSource.onChunkLoadError(chunk, e);

View file

@ -97,14 +97,19 @@ public class SingleSampleChunkSource implements ChunkSource {
return null; return null;
} }
@Override
public void onChunkLoadCompleted(Chunk chunk) {
// Do nothing.
}
@Override @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);
} }
} }

View file

@ -15,113 +15,59 @@
*/ */
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;
private volatile int bytesLoaded;
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 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.
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, * @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat) { * protected.
this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
sampleFormat, null);
}
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
* @param sampleFormat The format of the media contained by the chunk.
* @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
public boolean sampleAvailable() {
return isLoadFinished() && !isReadFinished();
}
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
if (!sampleAvailable()) {
return false;
}
int bytesLoaded = (int) bytesLoaded();
int sampleSize = bytesLoaded;
if (headerData != null) {
sampleSize += headerData.length;
}
if (holder.data == null || holder.data.capacity() < sampleSize) {
holder.replaceBuffer(sampleSize);
}
int bytesRead;
if (holder.data != null) {
if (headerData != null) {
holder.data.put(headerData);
}
bytesRead = inputStream.read(holder.data, bytesLoaded);
holder.size = sampleSize;
} else {
bytesRead = inputStream.skip(bytesLoaded);
holder.size = 0;
}
Assertions.checkState(bytesRead == bytesLoaded);
holder.timeUs = startTimeUs;
return true;
}
@Override
public void seekToStart() {
resetReadPosition();
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) {
resetReadPosition();
return true;
} }
@Override @Override
@ -131,7 +77,51 @@ public class SingleSampleMediaChunk extends MediaChunk {
@Override @Override
public DrmInitData getDrmInitData() { public DrmInitData getDrmInitData() {
return null; return sampleDrmInitData;
}
// Loadable implementation.
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override
public void load() throws IOException, InterruptedException {
if (!writtenHeader) {
if (headerData != null) {
getOutput().sampleData(new ParsableByteArray(headerData), headerData.length);
}
writtenHeader = true;
}
DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
try {
// Create and open the input.
dataSource.open(loadDataSpec);
// Load the sample data.
int result = 0;
while (result != C.RESULT_END_OF_INPUT) {
result = getOutput().sampleData(dataSource, Integer.MAX_VALUE);
if (result != C.RESULT_END_OF_INPUT) {
bytesLoaded += result;
}
}
int sampleSize = bytesLoaded;
if (headerData != null) {
sampleSize += headerData.length;
}
getOutput().sampleMetadata(startTimeUs, 0, sampleSize, 0, null);
} finally {
dataSource.close();
}
} }
} }

View file

@ -1,113 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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);
}

View file

@ -1,70 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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;
}
}

View file

@ -1,838 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.chunk.parser.mp4;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.mp4.Atom;
import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom;
import com.google.android.exoplayer.extractor.mp4.AtomParsers;
import com.google.android.exoplayer.extractor.mp4.DefaultSampleValues;
import com.google.android.exoplayer.extractor.mp4.Track;
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
import com.google.android.exoplayer.extractor.mp4.TrackFragment;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.H264Util;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;
/**
* Facilitates the extraction of data from the fragmented mp4 container format.
* <p>
* This implementation only supports de-muxed (i.e. single track) streams.
*/
public final class FragmentedMp4Extractor implements Extractor {
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
* the first sample in each segment.
* <p>
* This flag does nothing if the stream is not a video stream.
*/
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
// Parser states
private static final int STATE_READING_ATOM_HEADER = 0;
private static final int STATE_READING_ATOM_PAYLOAD = 1;
private static final int STATE_READING_ENCRYPTION_DATA = 2;
private static final int STATE_READING_SAMPLE = 3;
// Atoms that the parser cares about
private static final Set<Integer> PARSED_ATOMS;
static {
HashSet<Integer> parsedAtoms = new HashSet<Integer>();
parsedAtoms.add(Atom.TYPE_avc1);
parsedAtoms.add(Atom.TYPE_avc3);
parsedAtoms.add(Atom.TYPE_esds);
parsedAtoms.add(Atom.TYPE_hdlr);
parsedAtoms.add(Atom.TYPE_mdat);
parsedAtoms.add(Atom.TYPE_mdhd);
parsedAtoms.add(Atom.TYPE_moof);
parsedAtoms.add(Atom.TYPE_moov);
parsedAtoms.add(Atom.TYPE_mp4a);
parsedAtoms.add(Atom.TYPE_mvhd);
parsedAtoms.add(Atom.TYPE_sidx);
parsedAtoms.add(Atom.TYPE_stsd);
parsedAtoms.add(Atom.TYPE_tfdt);
parsedAtoms.add(Atom.TYPE_tfhd);
parsedAtoms.add(Atom.TYPE_tkhd);
parsedAtoms.add(Atom.TYPE_traf);
parsedAtoms.add(Atom.TYPE_trak);
parsedAtoms.add(Atom.TYPE_trex);
parsedAtoms.add(Atom.TYPE_trun);
parsedAtoms.add(Atom.TYPE_mvex);
parsedAtoms.add(Atom.TYPE_mdia);
parsedAtoms.add(Atom.TYPE_minf);
parsedAtoms.add(Atom.TYPE_stbl);
parsedAtoms.add(Atom.TYPE_pssh);
parsedAtoms.add(Atom.TYPE_saiz);
parsedAtoms.add(Atom.TYPE_uuid);
parsedAtoms.add(Atom.TYPE_senc);
parsedAtoms.add(Atom.TYPE_pasp);
PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
}
// Atoms that the parser considers to be containers
private static final Set<Integer> CONTAINER_TYPES;
static {
HashSet<Integer> atomContainerTypes = new HashSet<Integer>();
atomContainerTypes.add(Atom.TYPE_moov);
atomContainerTypes.add(Atom.TYPE_trak);
atomContainerTypes.add(Atom.TYPE_mdia);
atomContainerTypes.add(Atom.TYPE_minf);
atomContainerTypes.add(Atom.TYPE_stbl);
atomContainerTypes.add(Atom.TYPE_avcC);
atomContainerTypes.add(Atom.TYPE_moof);
atomContainerTypes.add(Atom.TYPE_traf);
atomContainerTypes.add(Atom.TYPE_mvex);
CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
}
private final int workaroundFlags;
// Parser state
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
private final Stack<ContainerAtom> containerAtoms;
private final TrackFragment fragmentRun;
private int parserState;
private int atomBytesRead;
private int rootAtomBytesRead;
private int atomType;
private int atomSize;
private ParsableByteArray atomData;
private int pendingSeekTimeMs;
private int sampleIndex;
private int pendingSeekSyncSampleIndex;
private int lastSyncSampleIndex;
// Data parsed from moov and sidx atoms
private DrmInitData.Mapped drmInitData;
private SegmentIndex segmentIndex;
private Track track;
private DefaultSampleValues extendsDefaults;
public FragmentedMp4Extractor() {
this(0);
}
/**
* @param workaroundFlags Flags to allow parsing of faulty streams.
* {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
*/
public FragmentedMp4Extractor(int workaroundFlags) {
this.workaroundFlags = workaroundFlags;
parserState = STATE_READING_ATOM_HEADER;
atomHeader = new ParsableByteArray(Atom.HEADER_SIZE);
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<ContainerAtom>();
fragmentRun = new TrackFragment();
}
/**
* Sideloads track information into the extractor.
*
* @param track The track to sideload.
*/
public void setTrack(Track track) {
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
this.track = track;
}
@Override
public DrmInitData getDrmInitData() {
return drmInitData;
}
@Override
public SegmentIndex getIndex() {
return segmentIndex;
}
@Override
public boolean hasRelativeIndexOffsets() {
return true;
}
@Override
public MediaFormat getFormat() {
return track == null ? null : track.mediaFormat;
}
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder out)
throws ParserException {
try {
int results = 0;
while ((results & READ_TERMINATING_RESULTS) == 0) {
switch (parserState) {
case STATE_READING_ATOM_HEADER:
results |= readAtomHeader(inputStream);
break;
case STATE_READING_ATOM_PAYLOAD:
results |= readAtomPayload(inputStream);
break;
case STATE_READING_ENCRYPTION_DATA:
results |= readEncryptionData(inputStream);
break;
default:
results |= readOrSkipSample(inputStream, out);
break;
}
}
return results;
} catch (Exception e) {
throw new ParserException(e);
}
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
pendingSeekTimeMs = (int) (seekTimeUs / 1000);
if (allowNoop && fragmentRun != null && fragmentRun.length > 0
&& pendingSeekTimeMs >= fragmentRun.getSamplePresentationTime(0)
&& pendingSeekTimeMs <= fragmentRun.getSamplePresentationTime(fragmentRun.length - 1)) {
int sampleIndexFound = 0;
int syncSampleIndexFound = 0;
for (int i = 0; i < fragmentRun.length; i++) {
if (fragmentRun.getSamplePresentationTime(i) <= pendingSeekTimeMs) {
if (fragmentRun.sampleIsSyncFrameTable[i]) {
syncSampleIndexFound = i;
}
sampleIndexFound = i;
}
}
if (syncSampleIndexFound == lastSyncSampleIndex && sampleIndexFound >= sampleIndex) {
pendingSeekTimeMs = 0;
return false;
}
}
containerAtoms.clear();
enterState(STATE_READING_ATOM_HEADER);
return true;
}
private void enterState(int state) {
switch (state) {
case STATE_READING_ATOM_HEADER:
atomBytesRead = 0;
if (containerAtoms.isEmpty()) {
rootAtomBytesRead = 0;
}
break;
}
parserState = state;
}
private int readAtomHeader(NonBlockingInputStream inputStream) {
int remainingBytes = Atom.HEADER_SIZE - atomBytesRead;
int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead != Atom.HEADER_SIZE) {
return RESULT_NEED_MORE_DATA;
}
atomHeader.setPosition(0);
atomSize = atomHeader.readInt();
atomType = atomHeader.readInt();
if (atomType == Atom.TYPE_mdat) {
if (fragmentRun.sampleEncryptionDataNeedsFill) {
enterState(STATE_READING_ENCRYPTION_DATA);
} else {
enterState(STATE_READING_SAMPLE);
}
return 0;
}
Integer atomTypeInteger = atomType; // Avoids boxing atomType twice.
if (PARSED_ATOMS.contains(atomTypeInteger)) {
if (CONTAINER_TYPES.contains(atomTypeInteger)) {
enterState(STATE_READING_ATOM_HEADER);
containerAtoms.add(new ContainerAtom(atomType,
rootAtomBytesRead + atomSize - Atom.HEADER_SIZE));
} else {
atomData = new ParsableByteArray(atomSize);
System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE);
enterState(STATE_READING_ATOM_PAYLOAD);
}
} else {
atomData = null;
enterState(STATE_READING_ATOM_PAYLOAD);
}
return 0;
}
private int readAtomPayload(NonBlockingInputStream inputStream) {
int bytesRead;
if (atomData != null) {
bytesRead = inputStream.read(atomData.data, atomBytesRead, atomSize - atomBytesRead);
} else {
bytesRead = inputStream.skip(atomSize - atomBytesRead);
}
if (bytesRead == -1) {
return RESULT_END_OF_STREAM;
}
rootAtomBytesRead += bytesRead;
atomBytesRead += bytesRead;
if (atomBytesRead != atomSize) {
return RESULT_NEED_MORE_DATA;
}
int results = 0;
if (atomData != null) {
results |= onLeafAtomRead(new LeafAtom(atomType, atomData));
}
while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
results |= onContainerAtomRead(containerAtoms.pop());
}
enterState(STATE_READING_ATOM_HEADER);
return results;
}
private int onLeafAtomRead(LeafAtom leaf) {
if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) {
segmentIndex = parseSidx(leaf.data);
return RESULT_READ_INDEX;
}
return 0;
}
private int onContainerAtomRead(ContainerAtom container) {
if (container.type == Atom.TYPE_moov) {
onMoovContainerAtomRead(container);
return RESULT_READ_INIT;
} else if (container.type == Atom.TYPE_moof) {
onMoofContainerAtomRead(container);
} else if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(container);
}
return 0;
}
private void onMoovContainerAtomRead(ContainerAtom moov) {
List<Atom.LeafAtom> moovChildren = moov.leafChildren;
int moovChildrenSize = moovChildren.size();
for (int i = 0; i < moovChildrenSize; i++) {
LeafAtom child = moovChildren.get(i);
if (child.type == Atom.TYPE_pssh) {
ParsableByteArray psshAtom = child.data;
psshAtom.setPosition(Atom.FULL_HEADER_SIZE);
UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
int dataSize = psshAtom.readInt();
byte[] data = new byte[dataSize];
psshAtom.readBytes(data, 0, dataSize);
if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4);
}
drmInitData.put(uuid, data);
}
}
ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
track = AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
moov.getLeafAtomOfType(Atom.TYPE_mvhd));
}
private void onMoofContainerAtomRead(ContainerAtom moof) {
fragmentRun.reset();
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
sampleIndex = 0;
lastSyncSampleIndex = 0;
pendingSeekSyncSampleIndex = 0;
if (pendingSeekTimeMs != 0) {
for (int i = 0; i < fragmentRun.length; i++) {
if (fragmentRun.sampleIsSyncFrameTable[i]) {
if (fragmentRun.getSamplePresentationTime(i) <= pendingSeekTimeMs) {
pendingSeekSyncSampleIndex = i;
}
}
}
pendingSeekTimeMs = 0;
}
}
/**
* Parses a trex atom (defined in 14496-12).
*/
private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
trex.setPosition(Atom.FULL_HEADER_SIZE + 4);
int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
int defaultSampleDuration = trex.readUnsignedIntToInt();
int defaultSampleSize = trex.readUnsignedIntToInt();
int defaultSampleFlags = trex.readInt();
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
defaultSampleSize, defaultSampleFlags);
}
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
out, workaroundFlags, extendedTypeScratch);
}
/**
* Parses a traf atom (defined in 14496-12).
*/
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
TrackEncryptionBox trackEncryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
parseSaiz(trackEncryptionBox, saiz.data, out);
}
LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
if (senc != null) {
parseSenc(senc.data, out);
}
int childrenSize = traf.leafChildren.size();
for (int i = 0; i < childrenSize; i++) {
LeafAtom atom = traf.leafChildren.get(i);
if (atom.type == Atom.TYPE_uuid) {
parseUuid(atom.data, out, extendedTypeScratch);
}
}
}
private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
TrackFragment out) {
int vectorSize = encryptionBox.initializationVectorSize;
saiz.setPosition(Atom.HEADER_SIZE);
int fullAtom = saiz.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01) == 1) {
saiz.skip(8);
}
int defaultSampleInfoSize = saiz.readUnsignedByte();
int sampleCount = saiz.readUnsignedIntToInt();
if (sampleCount != out.length) {
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
}
int totalSize = 0;
if (defaultSampleInfoSize == 0) {
boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
for (int i = 0; i < sampleCount; i++) {
int sampleInfoSize = saiz.readUnsignedByte();
totalSize += sampleInfoSize;
sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
}
} else {
boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
totalSize += defaultSampleInfoSize * sampleCount;
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
}
out.initEncryptionData(totalSize);
}
/**
* Parses a tfhd atom (defined in 14496-12).
*
* @param extendsDefaults Default sample values from the trex atom.
* @return The parsed default sample values.
*/
private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
ParsableByteArray tfhd) {
tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
tfhd.skip(4); // trackId
if ((flags & 0x01 /* base_data_offset_present */) != 0) {
tfhd.skip(8);
}
int defaultSampleDescriptionIndex =
((flags & 0x02 /* default_sample_description_index_present */) != 0) ?
tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex;
int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0) ?
tfhd.readUnsignedIntToInt() : extendsDefaults.duration;
int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0) ?
tfhd.readUnsignedIntToInt() : extendsDefaults.size;
int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0) ?
tfhd.readUnsignedIntToInt() : extendsDefaults.flags;
return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
defaultSampleSize, defaultSampleFlags);
}
/**
* Parses a tfdt atom (defined in 14496-12).
*
* @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
* media, expressed in the media's timescale.
*/
private static long parseTfdt(ParsableByteArray tfdt) {
tfdt.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfdt.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
}
/**
* Parses a trun atom (defined in 14496-12).
*
* @param track The corresponding track.
* @param defaultSampleValues Default sample values.
* @param decodeTime The decode time.
* @param trun The trun atom to parse.
* @param out The {@TrackFragment} into which parsed data should be placed.
*/
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
trun.setPosition(Atom.HEADER_SIZE);
int fullAtom = trun.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
int sampleCount = trun.readUnsignedIntToInt();
if ((flags & 0x01 /* data_offset_present */) != 0) {
trun.skip(4);
}
boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0;
int firstSampleFlags = defaultSampleValues.flags;
if (firstSampleFlagsPresent) {
firstSampleFlags = trun.readUnsignedIntToInt();
}
boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0;
boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0;
boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0;
boolean sampleCompositionTimeOffsetsPresent =
(flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
out.initTables(sampleCount);
int[] sampleSizeTable = out.sampleSizeTable;
int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
long timescale = track.timescale;
long cumulativeTime = decodeTime;
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
for (int i = 0; i < sampleCount; i++) {
// Use trun values if present, otherwise tfhd, otherwise trex.
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
: defaultSampleValues.duration;
int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
if (sampleCompositionTimeOffsetsPresent) {
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
// version 0 trun boxes, however a significant number of streams violate the spec and use
// signed integers instead. It's safe to always parse sample offsets as signed integers
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
int sampleOffset = trun.readInt();
sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
} else {
sampleCompositionTimeOffsetTable[i] = 0;
}
sampleDecodingTimeTable[i] = (cumulativeTime * 1000) / timescale;
sampleSizeTable[i] = sampleSize;
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
cumulativeTime += sampleDuration;
}
}
private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
byte[] extendedTypeScratch) {
uuid.setPosition(Atom.HEADER_SIZE);
uuid.readBytes(extendedTypeScratch, 0, 16);
// Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
return;
}
// Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
// audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
// Section 5.3.2.1."
parseSenc(uuid, 16, out);
}
private static void parseSenc(ParsableByteArray senc, TrackFragment out) {
parseSenc(senc, 0, out);
}
private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
senc.setPosition(Atom.HEADER_SIZE + offset);
int fullAtom = senc.readInt();
int flags = Atom.parseFullAtomFlags(fullAtom);
if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
// TODO: Implement this.
throw new IllegalStateException("Overriding TrackEncryptionBox parameters is unsupported");
}
boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
int sampleCount = senc.readUnsignedIntToInt();
if (sampleCount != out.length) {
throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
}
Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
out.initEncryptionData(senc.bytesLeft());
out.fillEncryptionData(senc);
}
/**
* Parses a sidx atom (defined in 14496-12).
*/
private static SegmentIndex parseSidx(ParsableByteArray atom) {
atom.setPosition(Atom.HEADER_SIZE);
int fullAtom = atom.readInt();
int version = Atom.parseFullAtomVersion(fullAtom);
atom.skip(4);
long timescale = atom.readUnsignedInt();
long earliestPresentationTime;
long firstOffset;
if (version == 0) {
earliestPresentationTime = atom.readUnsignedInt();
firstOffset = atom.readUnsignedInt();
} else {
earliestPresentationTime = atom.readUnsignedLongToLong();
firstOffset = atom.readUnsignedLongToLong();
}
atom.skip(2);
int referenceCount = atom.readUnsignedShort();
int[] sizes = new int[referenceCount];
long[] offsets = new long[referenceCount];
long[] durationsUs = new long[referenceCount];
long[] timesUs = new long[referenceCount];
long offset = firstOffset;
long time = earliestPresentationTime;
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt();
int type = 0x80000000 & firstInt;
if (type != 0) {
throw new IllegalStateException("Unhandled indirect reference");
}
long referenceDuration = atom.readUnsignedInt();
sizes[i] = 0x7fffffff & firstInt;
offsets[i] = offset;
// Calculate time and duration values such that any rounding errors are consistent. i.e. That
// timesUs[i] + durationsUs[i] == timesUs[i + 1].
timesUs[i] = timeUs;
time += referenceDuration;
timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
durationsUs[i] = timeUs - timesUs[i];
atom.skip(4);
offset += sizes[i];
}
return new SegmentIndex(atom.limit(), sizes, offsets, durationsUs, timesUs);
}
private int readEncryptionData(NonBlockingInputStream inputStream) {
boolean success = fragmentRun.fillEncryptionData(inputStream);
if (!success) {
return RESULT_NEED_MORE_DATA;
}
enterState(STATE_READING_SAMPLE);
return 0;
}
/**
* Attempts to read or skip the next sample in the current mdat atom.
* <p>
* If there are no more samples in the current mdat atom then the parser state is transitioned
* to {@link #STATE_READING_ATOM_HEADER} and 0 is returned.
* <p>
* If there's a pending seek to a sync frame, and if the next sample is before that frame, then
* the sample is skipped. Otherwise it is read.
* <p>
* It is possible for a sample to be read or skipped in part if there is insufficent data
* available from the {@link NonBlockingInputStream}. In this case the remainder of the sample
* can be read in a subsequent call passing the same {@link SampleHolder}.
*
* @param inputStream The stream from which to read the sample.
* @param out The holder into which to write the sample.
* @return A combination of RESULT_* flags indicating the result of the call.
*/
private int readOrSkipSample(NonBlockingInputStream inputStream, SampleHolder out) {
if (sampleIndex >= fragmentRun.length) {
// We've run out of samples in the current mdat atom.
enterState(STATE_READING_ATOM_HEADER);
return 0;
}
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
if (inputStream.getAvailableByteCount() < sampleSize) {
return RESULT_NEED_MORE_DATA;
}
if (sampleIndex < pendingSeekSyncSampleIndex) {
return skipSample(inputStream, sampleSize);
}
return readSample(inputStream, sampleSize, out);
}
private int skipSample(NonBlockingInputStream inputStream, int sampleSize) {
if (fragmentRun.definesEncryptionData) {
ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData;
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
sampleEncryptionData.skip(vectorSize);
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
if (subsampleEncryption) {
sampleEncryptionData.skip((2 + 4) * subsampleCount);
}
}
inputStream.skip(sampleSize);
sampleIndex++;
enterState(STATE_READING_SAMPLE);
return 0;
}
private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) {
if (out == null) {
return RESULT_NEED_SAMPLE_HOLDER;
}
out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
out.flags = 0;
if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
out.flags |= C.SAMPLE_FLAG_SYNC;
lastSyncSampleIndex = sampleIndex;
}
if (out.data == null || out.data.capacity() < sampleSize) {
out.replaceBuffer(sampleSize);
}
if (fragmentRun.definesEncryptionData) {
readSampleEncryptionData(fragmentRun.sampleEncryptionData, out);
}
ByteBuffer outputData = out.data;
if (outputData == null) {
inputStream.skip(sampleSize);
out.size = 0;
} else {
inputStream.read(outputData, sampleSize);
if (track.type == Track.TYPE_VIDEO) {
// The mp4 file contains length-prefixed NAL units, but the decoder wants start code
// delimited content.
H264Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize);
}
out.size = sampleSize;
}
sampleIndex++;
enterState(STATE_READING_SAMPLE);
return RESULT_READ_SAMPLE;
}
private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) {
TrackEncryptionBox encryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
if (!encryptionBox.isEncrypted) {
return;
}
byte[] keyId = encryptionBox.keyId;
int vectorSize = encryptionBox.initializationVectorSize;
boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
byte[] vector = out.cryptoInfo.iv;
if (vector == null || vector.length != 16) {
vector = new byte[16];
}
sampleEncryptionData.readBytes(vector, 0, vectorSize);
int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
int[] clearDataSizes = out.cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
clearDataSizes = new int[subsampleCount];
}
int[] encryptedDataSizes = out.cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
encryptedDataSizes = new int[subsampleCount];
}
if (subsampleEncryption) {
for (int i = 0; i < subsampleCount; i++) {
clearDataSizes[i] = sampleEncryptionData.readUnsignedShort();
encryptedDataSizes[i] = sampleEncryptionData.readUnsignedIntToInt();
}
} else {
clearDataSizes[0] = 0;
encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex];
}
out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector,
C.CRYPTO_MODE_AES_CTR);
out.flags |= C.SAMPLE_FLAG_ENCRYPTED;
}
}

View file

@ -1,550 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.chunk.parser.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Stack;
/**
* Default version of a 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 */ final class DefaultEbmlReader implements EbmlReader {
// State values used in variables state, elementIdState, elementContentSizeState, and
// varintBytesState.
private static final int STATE_BEGIN_READING = 0;
private static final int STATE_READ_CONTENTS = 1;
private static final int STATE_FINISHED_READING = 2;
/**
* The first byte of a variable-length integer (varint) will have one of these bit masks
* indicating the total length in bytes.
*
* <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
*/
private static final int[] VARINT_LENGTH_MASKS = new int[] {
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
};
private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
/**
* Scratch space to read in EBML varints, unsigned ints, and floats - each of which can be
* up to 8 bytes.
*/
private final byte[] tempByteArray = new byte[8];
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
/**
* Current {@link EbmlEventHandler} which is queried for element types
* and informed of element events.
*/
private EbmlEventHandler eventHandler;
/**
* Overall state for the current element. Must be one of the {@code STATE_*} constants.
*/
private int state;
/**
* Total bytes read since starting or the last {@link #reset()}.
*/
private long bytesRead;
/**
* The starting byte offset of the current element being parsed.
*/
private long elementOffset;
/**
* Holds the current element ID after {@link #elementIdState} is {@link #STATE_FINISHED_READING}.
*/
private int elementId;
/**
* State for the ID of the current element. Must be one of the {@code STATE_*} constants.
*/
private int elementIdState;
/**
* Holds the current element content size after {@link #elementContentSizeState}
* is {@link #STATE_FINISHED_READING}.
*/
private long elementContentSize;
/**
* State for the content size of the current element.
* Must be one of the {@code STATE_*} constants.
*/
private int elementContentSizeState;
/**
* State for the current variable-length integer (varint) being read into
* {@link #tempByteArray}. Must be one of the {@code STATE_*} constants.
*/
private int varintBytesState;
/**
* Length in bytes of the current variable-length integer (varint) being read into
* {@link #tempByteArray}.
*/
private int varintBytesLength;
/**
* Counts the number of bytes being contiguously read into either {@link #tempByteArray} or
* {@link #stringBytes}. Used to determine when all required bytes have been read across
* multiple calls.
*/
private int bytesState;
/**
* Holds string element bytes as they're being read in. Allocated after the element content
* size is known and released after calling {@link EbmlEventHandler#onStringElement(int, String)}.
*/
private byte[] stringBytes;
@Override
public void setEventHandler(EbmlEventHandler eventHandler) {
this.eventHandler = eventHandler;
}
@Override
public int read(NonBlockingInputStream inputStream) throws ParserException {
Assertions.checkState(eventHandler != null);
while (true) {
while (!masterElementsStack.isEmpty()
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId);
return READ_RESULT_CONTINUE;
}
if (state == STATE_BEGIN_READING) {
int idResult = readElementId(inputStream);
if (idResult != READ_RESULT_CONTINUE) {
return idResult;
}
int sizeResult = readElementContentSize(inputStream);
if (sizeResult != READ_RESULT_CONTINUE) {
return sizeResult;
}
state = STATE_READ_CONTENTS;
bytesState = 0;
}
int type = eventHandler.getElementType(elementId);
switch (type) {
case TYPE_MASTER:
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
eventHandler.onMasterElementStart(elementId, elementOffset, masterHeaderSize,
elementContentSize);
prepareForNextElement();
return READ_RESULT_CONTINUE;
case TYPE_UNSIGNED_INT:
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid integer size " + elementContentSize);
}
int intResult =
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
if (intResult != READ_RESULT_CONTINUE) {
return intResult;
}
long intValue = getTempByteArrayValue((int) elementContentSize, false);
eventHandler.onIntegerElement(elementId, intValue);
prepareForNextElement();
return READ_RESULT_CONTINUE;
case TYPE_FLOAT:
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
throw new IllegalStateException("Invalid float size " + elementContentSize);
}
int floatResult =
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
if (floatResult != READ_RESULT_CONTINUE) {
return floatResult;
}
long valueBits = getTempByteArrayValue((int) elementContentSize, false);
double floatValue;
if (elementContentSize == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
floatValue = Float.intBitsToFloat((int) valueBits);
} else {
floatValue = Double.longBitsToDouble(valueBits);
}
eventHandler.onFloatElement(elementId, floatValue);
prepareForNextElement();
return READ_RESULT_CONTINUE;
case TYPE_STRING:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"String element size " + elementContentSize + " is larger than MAX_INT");
}
if (stringBytes == null) {
stringBytes = new byte[(int) elementContentSize];
}
int stringResult =
readBytesInternal(inputStream, stringBytes, (int) elementContentSize);
if (stringResult != READ_RESULT_CONTINUE) {
return stringResult;
}
String stringValue = new String(stringBytes, Charset.forName(C.UTF8_NAME));
stringBytes = null;
eventHandler.onStringElement(elementId, stringValue);
prepareForNextElement();
return READ_RESULT_CONTINUE;
case TYPE_BINARY:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"Binary element size " + elementContentSize + " is larger than MAX_INT");
}
if (inputStream.getAvailableByteCount() < elementContentSize) {
return READ_RESULT_NEED_MORE_DATA;
}
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
boolean consumed = eventHandler.onBinaryElement(
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
if (consumed) {
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
if (expectedBytesRead != bytesRead) {
throw new IllegalStateException("Incorrect total bytes read. Expected "
+ expectedBytesRead + " but actually " + bytesRead);
}
prepareForNextElement();
}
return READ_RESULT_CONTINUE;
case TYPE_UNKNOWN:
if (elementContentSize > Integer.MAX_VALUE) {
throw new IllegalStateException(
"Unknown element size " + elementContentSize + " is larger than MAX_INT");
}
int skipResult = skipBytesInternal(inputStream, (int) elementContentSize);
if (skipResult != READ_RESULT_CONTINUE) {
return skipResult;
}
prepareForNextElement();
break;
default:
throw new IllegalStateException("Invalid element type " + type);
}
}
}
@Override
public long getBytesRead() {
return bytesRead;
}
@Override
public void reset() {
prepareForNextElement();
masterElementsStack.clear();
bytesRead = 0;
}
@Override
public long readVarint(NonBlockingInputStream inputStream) {
varintBytesState = STATE_BEGIN_READING;
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read varint");
}
return getTempByteArrayValue(varintBytesLength, true);
}
@Override
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
bytesState = 0;
int result = readBytesInternal(inputStream, byteBuffer, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read bytes into buffer");
}
}
@Override
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
bytesState = 0;
int result = readBytesInternal(inputStream, byteArray, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't read bytes into array");
}
}
@Override
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
bytesState = 0;
int result = skipBytesInternal(inputStream, totalBytes);
if (result != READ_RESULT_CONTINUE) {
throw new IllegalStateException("Couldn't skip bytes");
}
}
/**
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
* reading a new element from scratch.
*/
private void prepareForNextElement() {
state = STATE_BEGIN_READING;
elementIdState = STATE_BEGIN_READING;
elementContentSizeState = STATE_BEGIN_READING;
elementOffset = bytesRead;
}
/**
* Reads an element ID such that reading can be stopped and started again in a later call
* if not enough bytes are available. Returns {@link #READ_RESULT_CONTINUE} if a full element ID
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
*
* @param inputStream The input stream from which an element ID should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readElementId(NonBlockingInputStream inputStream) {
if (elementIdState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
if (elementIdState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementIdState = STATE_READ_CONTENTS;
}
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
return result;
}
// Element IDs are at most 4 bytes so cast to int now.
elementId = (int) getTempByteArrayValue(varintBytesLength, false);
elementIdState = STATE_FINISHED_READING;
return READ_RESULT_CONTINUE;
}
/**
* Reads an element's content size such that reading can be stopped and started again in a later
* call if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire element size has been
* read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
*
* @param inputStream The input stream from which an element size should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readElementContentSize(NonBlockingInputStream inputStream) {
if (elementContentSizeState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
if (elementContentSizeState == STATE_BEGIN_READING) {
varintBytesState = STATE_BEGIN_READING;
elementContentSizeState = STATE_READ_CONTENTS;
}
int result = readVarintBytes(inputStream);
if (result != READ_RESULT_CONTINUE) {
return result;
}
elementContentSize = getTempByteArrayValue(varintBytesLength, true);
elementContentSizeState = STATE_FINISHED_READING;
return READ_RESULT_CONTINUE;
}
/**
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
* again in a later call if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire varint has been read into
* {@link #tempByteArray} and the length of the varint is in {@link #varintBytesLength}.
* Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} before calling to indicate
* a new varint should be read.
*
* @param inputStream The input stream from which a varint should be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readVarintBytes(NonBlockingInputStream inputStream) {
if (varintBytesState == STATE_FINISHED_READING) {
return READ_RESULT_CONTINUE;
}
// Read first byte to get length.
if (varintBytesState == STATE_BEGIN_READING) {
bytesState = 0;
int result = readBytesInternal(inputStream, tempByteArray, 1);
if (result != READ_RESULT_CONTINUE) {
return result;
}
varintBytesState = STATE_READ_CONTENTS;
int firstByte = tempByteArray[0] & 0xff;
varintBytesLength = -1;
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
varintBytesLength = i + 1;
break;
}
}
if (varintBytesLength == -1) {
throw new IllegalStateException(
"No valid varint length mask found at bytesRead = " + bytesRead);
}
}
// Read remaining bytes.
int result = readBytesInternal(inputStream, tempByteArray, varintBytesLength);
if (result != READ_RESULT_CONTINUE) {
return result;
}
// All bytes have been read.
return READ_RESULT_CONTINUE;
}
/**
* Reads a set amount of bytes into a {@link ByteBuffer} such that reading can be stopped
* and started again later if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* @param inputStream The input stream from which bytes should be read
* @param byteBuffer The {@link ByteBuffer} into which bytes should be read
* @param totalBytes The total size of bytes to be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readBytesInternal(
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
if (bytesState == STATE_BEGIN_READING && totalBytes > byteBuffer.capacity()) {
throw new IllegalArgumentException("Byte buffer not large enough");
}
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.read(byteBuffer, remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Reads a set amount of bytes into a {@code byte[]} such that reading can be stopped
* and started again later if not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
*
* @param inputStream The input stream from which bytes should be read
* @param byteArray The {@code byte[]} into which bytes should be read
* @param totalBytes The total size of bytes to be read
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int readBytesInternal(
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
if (bytesState == STATE_BEGIN_READING && totalBytes > byteArray.length) {
throw new IllegalArgumentException("Byte array not large enough");
}
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.read(byteArray, bytesState, remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Skips a set amount of bytes such that reading can be stopped and started again later if
* not enough bytes are available.
*
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been skipped. Reset
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes
* should be skipped.
*
* @param inputStream The input stream from which bytes should be skipped
* @param totalBytes The total size of bytes to be skipped
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int skipBytesInternal(NonBlockingInputStream inputStream, int totalBytes) {
if (bytesState >= totalBytes) {
return READ_RESULT_CONTINUE;
}
int remainingBytes = totalBytes - bytesState;
int additionalBytesRead = inputStream.skip(remainingBytes);
return updateBytesState(additionalBytesRead, totalBytes);
}
/**
* Updates {@link #bytesState} and {@link #bytesRead} after reading bytes in one of the
* {@code verbBytesInternal} methods.
*
* @param additionalBytesRead The number of additional bytes read to be accounted for
* @param totalBytes The total size of bytes to be read or skipped
* @return One of the {@code RESULT_*} flags defined in this class
*/
private int updateBytesState(int additionalBytesRead, int totalBytes) {
if (additionalBytesRead == -1) {
return READ_RESULT_END_OF_STREAM;
}
bytesState += additionalBytesRead;
bytesRead += additionalBytesRead;
if (bytesState < totalBytes) {
return READ_RESULT_NEED_MORE_DATA;
} else {
return READ_RESULT_CONTINUE;
}
}
/**
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
*
* @param byteLength The number of bytes to parse from {@link #tempByteArray}
* @param removeLengthMask Removes the variable-length integer length mask from the value
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used
*/
private long getTempByteArrayValue(int byteLength, boolean removeLengthMask) {
if (removeLengthMask) {
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
}
long varint = 0;
for (int i = 0; i < byteLength; i++) {
// Shift all existing bits up one byte and add the next byte at the bottom.
varint = (varint << 8) | (tempByteArray[i] & 0xff);
}
return varint;
}
/**
* Used in {@link #masterElementsStack} to track when the current master element ends so that
* {@link EbmlEventHandler#onMasterElementEnd(int)} is called.
*/
private static final class MasterElement {
private final int elementId;
private final long elementEndOffsetBytes;
private MasterElement(int elementId, long elementEndOffsetBytes) {
this.elementId = elementId;
this.elementEndOffsetBytes = elementEndOffsetBytes;
}
}
}

View file

@ -1,125 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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;
}

View file

@ -1,109 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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);
}

View file

@ -1,734 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.chunk.parser.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* An extractor to facilitate data retrieval from the WebM container format.
*
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
* RFC on encrypted WebM can be found
* <a href="http://wiki.webmproject.org/encryption/webm-encryption-rfc">here</a>.
*/
public final class WebmExtractor implements Extractor {
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
private static final String CODEC_ID_VORBIS = "A_VORBIS";
private static final String CODEC_ID_OPUS = "A_OPUS";
private static final int VORBIS_MAX_INPUT_SIZE = 8192;
private static final int OPUS_MAX_INPUT_SIZE = 5760;
private static final int BLOCK_COUNTER_SIZE = 16;
private static final int UNKNOWN = -1;
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_INFO = 0x1549A966;
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
private static final int ID_DURATION = 0x4489;
private static final int ID_CLUSTER = 0x1F43B675;
private static final int ID_TIME_CODE = 0xE7;
private static final int ID_SIMPLE_BLOCK = 0xA3;
private static final int ID_BLOCK_GROUP = 0xA0;
private static final int ID_BLOCK = 0xA1;
private static final int ID_TRACKS = 0x1654AE6B;
private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_CODEC_ID = 0x86;
private static final int ID_CODEC_PRIVATE = 0x63A2;
private static final int ID_CODEC_DELAY = 0x56AA;
private static final int ID_SEEK_PRE_ROLL = 0x56BB;
private static final int ID_VIDEO = 0xE0;
private static final int ID_PIXEL_WIDTH = 0xB0;
private static final int ID_PIXEL_HEIGHT = 0xBA;
private static final int ID_AUDIO = 0xE1;
private static final int ID_CHANNELS = 0x9F;
private static final int ID_SAMPLING_FREQUENCY = 0xB5;
private static final int ID_CONTENT_ENCODINGS = 0x6D80;
private static final int ID_CONTENT_ENCODING = 0x6240;
private static final int ID_CONTENT_ENCODING_ORDER = 0x5031;
private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032;
private static final int ID_CONTENT_ENCODING_TYPE = 0x5033;
private static final int ID_CONTENT_ENCRYPTION = 0x5035;
private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1;
private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2;
private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7;
private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8;
private static final int ID_CUES = 0x1C53BB6B;
private static final int ID_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int LACING_NONE = 0;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private DrmInitData.Universal drmInitData;
private SampleHolder sampleHolder;
private int readResults;
private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = C.UNKNOWN_TIME_US;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private int channelCount = UNKNOWN;
private int sampleRate = UNKNOWN;
private byte[] codecPrivate;
private String codecId;
private long codecDelayNs;
private long seekPreRollNs;
private boolean isAudioTrack;
private boolean hasContentEncryption;
private byte[] encryptionKeyId;
private long cuesSizeBytes = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
private MediaFormat format;
private SegmentIndex cues;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
public WebmExtractor() {
this(new DefaultEbmlReader());
}
/* package */ WebmExtractor(EbmlReader reader) {
this.reader = reader;
this.reader.setEventHandler(new InnerEbmlEventHandler());
}
@Override
public int read(
NonBlockingInputStream inputStream, SampleHolder sampleHolder) throws ParserException {
this.sampleHolder = sampleHolder;
this.readResults = 0;
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
int ebmlReadResult = reader.read(inputStream);
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
}
}
this.sampleHolder = null;
return readResults;
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
if (allowNoop
&& cues != null
&& clusterTimecodeUs != UNKNOWN
&& simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
clusterTimecodeUs = UNKNOWN;
simpleBlockTimecodeUs = UNKNOWN;
reader.reset();
return true;
}
@Override
public SegmentIndex getIndex() {
return cues;
}
@Override
public boolean hasRelativeIndexOffsets() {
return false;
}
@Override
public MediaFormat getFormat() {
return format;
}
@Override
public DrmInitData getDrmInitData() {
return drmInitData;
}
/* package */ int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
case ID_INFO:
case ID_CLUSTER:
case ID_TRACKS:
case ID_TRACK_ENTRY:
case ID_AUDIO:
case ID_VIDEO:
case ID_CONTENT_ENCODINGS:
case ID_CONTENT_ENCODING:
case ID_CONTENT_ENCRYPTION:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS:
case ID_CUES:
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
case ID_BLOCK_GROUP:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
case ID_TIMECODE_SCALE:
case ID_TIME_CODE:
case ID_PIXEL_WIDTH:
case ID_PIXEL_HEIGHT:
case ID_CODEC_DELAY:
case ID_SEEK_PRE_ROLL:
case ID_CHANNELS:
case ID_CONTENT_ENCODING_ORDER:
case ID_CONTENT_ENCODING_SCOPE:
case ID_CONTENT_ENCODING_TYPE:
case ID_CONTENT_ENCRYPTION_ALGORITHM:
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_CONTENT_ENCRYPTION_KEY_ID:
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
case ID_CODEC_PRIVATE:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
case ID_SAMPLING_FREQUENCY:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
/* package */ boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes,
long contentsSizeBytes) throws ParserException {
switch (id) {
case ID_SEGMENT:
if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
throw new ParserException("Multiple Segment elements not supported");
}
segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
break;
case ID_CUES:
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
break;
case ID_CONTENT_ENCODING:
// TODO: check and fail if more than one content encoding is present.
break;
case ID_CONTENT_ENCRYPTION:
hasContentEncryption = true;
break;
default:
// pass
}
return true;
}
/* package */ boolean onMasterElementEnd(int id) throws ParserException {
switch (id) {
case ID_CUES:
buildCues();
return false;
case ID_CONTENT_ENCODING:
if (!hasContentEncryption) {
// We found a ContentEncoding other than Encryption.
throw new ParserException("Found an unsupported ContentEncoding");
}
if (encryptionKeyId == null) {
throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
}
drmInitData = new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, encryptionKeyId);
return true;
case ID_AUDIO:
isAudioTrack = true;
return true;
case ID_TRACK_ENTRY:
if (isAudioTrack) {
buildAudioFormat();
} else {
buildVideoFormat();
}
return true;
default:
return true;
}
}
/* package */ boolean onIntegerElement(int id, long value) throws ParserException {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
if (value != 1) {
throw new ParserException("EBMLReadVersion " + value + " not supported");
}
break;
case ID_DOC_TYPE_READ_VERSION:
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
if (value < 1 || value > 2) {
throw new ParserException("DocTypeReadVersion " + value + " not supported");
}
break;
case ID_TIMECODE_SCALE:
timecodeScale = value;
break;
case ID_PIXEL_WIDTH:
pixelWidth = (int) value;
break;
case ID_PIXEL_HEIGHT:
pixelHeight = (int) value;
break;
case ID_CODEC_DELAY:
codecDelayNs = value;
break;
case ID_SEEK_PRE_ROLL:
seekPreRollNs = value;
break;
case ID_CHANNELS:
channelCount = (int) value;
break;
case ID_CONTENT_ENCODING_ORDER:
// This extractor only supports one ContentEncoding element and hence the order has to be 0.
if (value != 0) {
throw new ParserException("ContentEncodingOrder " + value + " not supported");
}
break;
case ID_CONTENT_ENCODING_SCOPE:
// This extractor only supports the scope of all frames (since that's the only scope used
// for Encryption).
if (value != 1) {
throw new ParserException("ContentEncodingScope " + value + " not supported");
}
break;
case ID_CONTENT_ENCODING_TYPE:
// This extractor only supports Encrypted ContentEncodingType.
if (value != 1) {
throw new ParserException("ContentEncodingType " + value + " not supported");
}
break;
case ID_CONTENT_ENCRYPTION_ALGORITHM:
// Only the value 5 (AES) is allowed according to the WebM specification.
if (value != 5) {
throw new ParserException("ContentEncAlgo " + value + " not supported");
}
break;
case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE:
// Only the value 1 is allowed according to the WebM specification.
if (value != 1) {
throw new ParserException("AESSettingsCipherMode " + value + " not supported");
}
break;
case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value));
break;
case ID_CUE_CLUSTER_POSITION:
cueClusterPositions.add(value);
break;
case ID_TIME_CODE:
clusterTimecodeUs = scaleTimecodeToUs(value);
break;
default:
// pass
}
return true;
}
/* package */ boolean onFloatElement(int id, double value) {
switch (id) {
case ID_DURATION:
durationUs = scaleTimecodeToUs((long) value);
break;
case ID_SAMPLING_FREQUENCY:
sampleRate = (int) value;
break;
default:
// pass
}
return true;
}
/* package */ boolean onStringElement(int id, String value) throws ParserException {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
if (!DOC_TYPE_WEBM.equals(value)) {
throw new ParserException("DocType " + value + " not supported");
}
break;
case ID_CODEC_ID:
// Validate that CodecID is supported. This extractor only supports "V_VP9" and "A_VORBIS".
if (!isCodecSupported(value)) {
throw new ParserException("CodecID " + value + " not supported");
}
codecId = value;
break;
default:
// pass
}
return true;
}
/* package */ boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) throws ParserException {
switch (id) {
case ID_SIMPLE_BLOCK:
case ID_BLOCK:
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// and http://matroska.org/technical/specs/index.html#block_structure
// for info about how data is organized in SimpleBlock and Block elements respectively. They
// differ only in the way flags are specified.
// If we don't have a sample holder then don't consume the data.
if (sampleHolder == null) {
readResults |= RESULT_NEED_SAMPLE_HOLDER;
return false;
}
// Value of trackNumber is not used but needs to be read.
reader.readVarint(inputStream);
// Next three bytes have timecode and flags.
reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
// First two bytes of the three are the relative timecode.
int timecode =
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
long timecodeUs = scaleTimecodeToUs(timecode);
// Last byte of the three has some flags and the lacing value.
boolean keyframe;
if (id == ID_BLOCK) {
// Matroska Block element does not self-sufficiently say whether it is a key frame or not.
// It depends on the existence of another element (ReferenceBlock) which may occur after
// the Block element. Since this extractor uses Block element only for Opus, we set the
// keyframe to be true always since all Opus frames are key frames.
keyframe = true;
} else {
keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
}
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
if (lacing != LACING_NONE) {
throw new ParserException("Lacing mode " + lacing + " not supported");
}
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.flags = (keyframe ? C.SAMPLE_FLAG_SYNC : 0)
| (invisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0);
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
if (hasContentEncryption) {
byte[] signalByte = new byte[1];
reader.readBytes(inputStream, signalByte, 1);
sampleHolder.size -= 1;
// First bit of the signalByte (extension bit) must be 0.
if ((signalByte[0] & 0x80) != 0) {
throw new ParserException("Extension bit is set in signal byte");
}
boolean isEncrypted = (signalByte[0] & 0x01) == 0x01;
if (isEncrypted) {
byte[] iv = null;
iv = sampleHolder.cryptoInfo.iv;
if (iv == null || iv.length != BLOCK_COUNTER_SIZE) {
iv = new byte[BLOCK_COUNTER_SIZE];
}
reader.readBytes(inputStream, iv, 8); // The container has only 8 bytes of IV.
sampleHolder.size -= 8;
int[] clearDataSizes = sampleHolder.cryptoInfo.numBytesOfClearData;
if (clearDataSizes == null || clearDataSizes.length < 1) {
clearDataSizes = new int[1];
}
int[] encryptedDataSizes = sampleHolder.cryptoInfo.numBytesOfEncryptedData;
if (encryptedDataSizes == null || encryptedDataSizes.length < 1) {
encryptedDataSizes = new int[1];
}
clearDataSizes[0] = 0;
encryptedDataSizes[0] = sampleHolder.size;
sampleHolder.cryptoInfo.set(1, clearDataSizes, encryptedDataSizes,
encryptionKeyId, iv, C.CRYPTO_MODE_AES_CTR);
sampleHolder.flags |= C.SAMPLE_FLAG_ENCRYPTED;
}
}
if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) {
sampleHolder.replaceBuffer(sampleHolder.size);
}
ByteBuffer outputData = sampleHolder.data;
if (outputData == null) {
reader.skipBytes(inputStream, sampleHolder.size);
sampleHolder.size = 0;
} else {
reader.readBytes(inputStream, outputData, sampleHolder.size);
}
readResults |= RESULT_READ_SAMPLE;
break;
case ID_CODEC_PRIVATE:
codecPrivate = new byte[contentsSizeBytes];
reader.readBytes(inputStream, codecPrivate, contentsSizeBytes);
break;
case ID_CONTENT_ENCRYPTION_KEY_ID:
encryptionKeyId = new byte[contentsSizeBytes];
reader.readBytes(inputStream, encryptionKeyId, contentsSizeBytes);
break;
default:
// pass
}
return true;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
private boolean isCodecSupported(String codecId) {
return CODEC_ID_VP9.equals(codecId)
|| CODEC_ID_OPUS.equals(codecId)
|| CODEC_ID_VORBIS.equals(codecId);
}
/**
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
*
* <p>Replaces the previous {@link #format} only if video width/height have changed.
* {@link #format} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link ParserException} will be thrown.
*/
private void buildVideoFormat() throws ParserException {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, durationUs,
pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT;
} else if (format == null) {
throw new ParserException("Unable to build format");
}
}
/**
* Build an audio {@link MediaFormat} containing recently gathered Audio information, if needed.
*
* <p>Replaces the previous {@link #format} only if audio channel count/sample rate have changed.
* {@link #format} is guaranteed to not be null after calling this method.
*
* @throws ParserException If an error occurs when parsing codec's private data or if the format
* can't be built.
*/
private void buildAudioFormat() throws ParserException {
if (channelCount != UNKNOWN && sampleRate != UNKNOWN
&& (format == null || format.channelCount != channelCount
|| format.sampleRate != sampleRate)) {
if (CODEC_ID_VORBIS.equals(codecId)) {
format = MediaFormat.createAudioFormat(MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE,
durationUs, channelCount, sampleRate, parseVorbisCodecPrivate());
} else if (CODEC_ID_OPUS.equals(codecId)) {
ArrayList<byte[]> opusInitializationData = new ArrayList<byte[]>(3);
opusInitializationData.add(codecPrivate);
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(codecDelayNs).array());
opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(seekPreRollNs).array());
format = MediaFormat.createAudioFormat(MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE,
durationUs, channelCount, sampleRate, opusInitializationData);
}
readResults |= RESULT_READ_INIT;
} else if (format == null) {
throw new ParserException("Unable to build format");
}
}
/**
* Build a {@link SegmentIndex} containing recently gathered Cues information.
*
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link ParserException} will be thrown.
*/
private void buildCues() throws ParserException {
if (segmentStartOffsetBytes == UNKNOWN) {
throw new ParserException("Segment start/end offsets unknown");
} else if (durationUs == C.UNKNOWN_TIME_US) {
throw new ParserException("Duration unknown");
} else if (cuesSizeBytes == UNKNOWN) {
throw new ParserException("Cues size unknown");
} else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new ParserException("Invalid/missing cue points");
}
int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize];
long[] offsets = new long[cuePointsSize];
long[] durationsUs = new long[cuePointsSize];
long[] timesUs = new long[cuePointsSize];
for (int i = 0; i < cuePointsSize; i++) {
timesUs[i] = cueTimesUs.get(i);
offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
}
for (int i = 0; i < cuePointsSize - 1; i++) {
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
durationsUs[i] = timesUs[i + 1] - timesUs[i];
}
sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
readResults |= RESULT_READ_INDEX;
}
/**
* Parses Vorbis Codec Private data and adds it as initialization data to the {@link #format}.
* WebM Vorbis Codec Private data specification can be found
* <a href="http://matroska.org/technical/specs/codecid/index.html">here</a>.
*
* @return ArrayList of byte arrays containing the initialization data on success.
* @throws ParserException If parsing codec private data fails.
*/
private ArrayList<byte[]> parseVorbisCodecPrivate() throws ParserException {
try {
if (codecPrivate[0] != 0x02) {
throw new ParserException("Error parsing vorbis codec private");
}
int offset = 1;
int vorbisInfoLength = 0;
while (codecPrivate[offset] == (byte) 0xFF) {
vorbisInfoLength += 0xFF;
offset++;
}
vorbisInfoLength += codecPrivate[offset++];
int vorbisSkipLength = 0;
while (codecPrivate[offset] == (byte) 0xFF) {
vorbisSkipLength += 0xFF;
offset++;
}
vorbisSkipLength += codecPrivate[offset++];
if (codecPrivate[offset] != 0x01) {
throw new ParserException("Error parsing vorbis codec private");
}
byte[] vorbisInfo = new byte[vorbisInfoLength];
System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);
offset += vorbisInfoLength;
if (codecPrivate[offset] != 0x03) {
throw new ParserException("Error parsing vorbis codec private");
}
offset += vorbisSkipLength;
if (codecPrivate[offset] != 0x05) {
throw new ParserException("Error parsing vorbis codec private");
}
byte[] vorbisBooks = new byte[codecPrivate.length - offset];
System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);
ArrayList<byte[]> initializationData = new ArrayList<byte[]>(2);
initializationData.add(vorbisInfo);
initializationData.add(vorbisBooks);
return initializationData;
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParserException("Error parsing vorbis codec private");
}
}
/**
* Passes events through to {@link WebmExtractor} as
* callbacks from {@link EbmlReader} are received.
*/
private final class InnerEbmlEventHandler implements EbmlEventHandler {
@Override
public int getElementType(int id) {
return WebmExtractor.this.getElementType(id);
}
@Override
public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes,
long contentsSizeBytes) throws ParserException {
WebmExtractor.this.onMasterElementStart(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
}
@Override
public void onMasterElementEnd(int id) throws ParserException {
WebmExtractor.this.onMasterElementEnd(id);
}
@Override
public void onIntegerElement(int id, long value) throws ParserException {
WebmExtractor.this.onIntegerElement(id, value);
}
@Override
public void onFloatElement(int id, double value) {
WebmExtractor.this.onFloatElement(id, value);
}
@Override
public void onStringElement(int id, String value) throws ParserException {
WebmExtractor.this.onStringElement(id, value);
}
@Override
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) throws ParserException {
return WebmExtractor.this.onBinaryElement(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
}
}
}

View file

@ -17,10 +17,10 @@ package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
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.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;
@ -28,11 +28,9 @@ import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator; 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.InitializationChunk;
import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer.chunk.SingleSampleMediaChunk;
import com.google.android.exoplayer.chunk.parser.Extractor;
import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.chunk.parser.webm.WebmExtractor;
import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.ContentProtection; import com.google.android.exoplayer.dash.mpd.ContentProtection;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
@ -40,15 +38,17 @@ import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.ChunkIndex;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.extractor.webm.WebmExtractor;
import com.google.android.exoplayer.text.webvtt.WebvttParser; import com.google.android.exoplayer.text.webvtt.WebvttParser;
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.Clock;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.SystemClock;
import android.net.Uri;
import android.os.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
@ -83,6 +83,7 @@ public class DashChunkSource implements ChunkSource {
private final DataSource dataSource; private final DataSource dataSource;
private final FormatEvaluator evaluator; private final FormatEvaluator evaluator;
private final Evaluation evaluation; private final Evaluation evaluation;
private final Clock systemClock;
private final StringBuilder headerBuilder; private final StringBuilder headerBuilder;
private final long liveEdgeLatencyUs; private final long liveEdgeLatencyUs;
private final long elapsedRealtimeOffsetUs; private final long elapsedRealtimeOffsetUs;
@ -95,8 +96,8 @@ public class DashChunkSource implements ChunkSource {
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher; private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex; private final int adaptationSetIndex;
private final int[] representationIndices; private final int[] representationIndices;
private final DrmInitData drmInitData;
private DrmInitData drmInitData;
private MediaPresentationDescription currentManifest; private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest; private boolean finishedCurrentManifest;
@ -140,8 +141,8 @@ public class DashChunkSource implements ChunkSource {
*/ */
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0, this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator,
0); new SystemClock(), 0, 0);
} }
/** /**
@ -171,19 +172,21 @@ public class DashChunkSource implements ChunkSource {
int adaptationSetIndex, int[] representationIndices, DataSource dataSource, int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) { FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) {
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000); dataSource, formatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000,
elapsedRealtimeOffsetMs * 1000);
} }
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher, /* package */ DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
MediaPresentationDescription initialManifest, int adaptationSetIndex, MediaPresentationDescription initialManifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) { Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) {
this.manifestFetcher = manifestFetcher; this.manifestFetcher = manifestFetcher;
this.currentManifest = initialManifest; this.currentManifest = initialManifest;
this.adaptationSetIndex = adaptationSetIndex; this.adaptationSetIndex = adaptationSetIndex;
this.representationIndices = representationIndices; this.representationIndices = representationIndices;
this.dataSource = dataSource; this.dataSource = dataSource;
this.evaluator = formatEvaluator; this.evaluator = formatEvaluator;
this.systemClock = systemClock;
this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.liveEdgeLatencyUs = liveEdgeLatencyUs;
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs; this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs;
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
@ -207,7 +210,7 @@ public class DashChunkSource implements ChunkSource {
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
: new FragmentedMp4Extractor(); : new FragmentedMp4Extractor();
representationHolders.put(formats[i].id, representationHolders.put(formats[i].id,
new RepresentationHolder(representations[i], extractor)); new RepresentationHolder(representations[i], new ChunkExtractorWrapper(extractor)));
} }
this.maxWidth = maxWidth; this.maxWidth = maxWidth;
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
@ -276,7 +279,7 @@ public class DashChunkSource implements ChunkSource {
minUpdatePeriod = 5000; minUpdatePeriod = 5000;
} }
if (finishedCurrentManifest && (SystemClock.elapsedRealtime() if (finishedCurrentManifest && (android.os.SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadTimestamp() + minUpdatePeriod)) { > manifestFetcher.getManifestLoadTimestamp() + minUpdatePeriod)) {
manifestFetcher.requestRefresh(); manifestFetcher.requestRefresh();
} }
@ -310,12 +313,12 @@ public class DashChunkSource implements ChunkSource {
RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id); RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id);
Representation selectedRepresentation = representationHolder.representation; Representation selectedRepresentation = representationHolder.representation;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex; DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
Extractor extractor = representationHolder.extractor; ChunkExtractorWrapper extractorWrapper = representationHolder.extractorWrapper;
RangedUri pendingInitializationUri = null; RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null; RangedUri pendingIndexUri = null;
if (extractor.getFormat() == null) { if (representationHolder.format == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri(); pendingInitializationUri = selectedRepresentation.getInitializationUri();
} }
if (segmentIndex == null) { if (segmentIndex == null) {
@ -325,7 +328,7 @@ public class DashChunkSource implements ChunkSource {
if (pendingInitializationUri != null || pendingIndexUri != null) { if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make. // We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractor, dataSource, evaluation.trigger); selectedRepresentation, extractorWrapper, dataSource, evaluation.trigger);
lastChunkWasInitialization = true; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
@ -333,7 +336,7 @@ public class DashChunkSource implements ChunkSource {
long nowUs; long nowUs;
if (elapsedRealtimeOffsetUs != 0) { if (elapsedRealtimeOffsetUs != 0) {
nowUs = (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; nowUs = (systemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs;
} else { } else {
nowUs = System.currentTimeMillis() * 1000; nowUs = System.currentTimeMillis() * 1000;
} }
@ -362,8 +365,9 @@ public class DashChunkSource implements ChunkSource {
} }
segmentNum = segmentIndex.getSegmentNum(seekPositionUs); segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
} else { } else {
segmentNum = queue.get(out.queueSize - 1).nextChunkIndex MediaChunk previous = queue.get(out.queueSize - 1);
- representationHolder.segmentNumShift; segmentNum = previous.isLastChunk ? -1
: previous.chunkIndex + 1 - representationHolder.segmentNumShift;
} }
if (currentManifest.dynamic) { if (currentManifest.dynamic) {
@ -401,6 +405,26 @@ public class DashChunkSource implements ChunkSource {
: (manifestFetcher != null ? manifestFetcher.getError() : null); : (manifestFetcher != null ? manifestFetcher.getError() : null);
} }
@Override
public void onChunkLoadCompleted(Chunk chunk) {
if (chunk instanceof InitializationChunk) {
InitializationChunk initializationChunk = (InitializationChunk) chunk;
String formatId = initializationChunk.format.id;
RepresentationHolder representationHolder = representationHolders.get(formatId);
if (initializationChunk.hasFormat()) {
representationHolder.format = initializationChunk.getFormat();
}
if (initializationChunk.hasSeekMap()) {
representationHolder.segmentIndex = new DashWrappingSegmentIndex(
(ChunkIndex) initializationChunk.getSeekMap(),
initializationChunk.dataSpec.uri.toString());
}
if (initializationChunk.hasDrmInitData()) {
drmInitData = initializationChunk.getDrmInitData();
}
}
}
@Override @Override
public void onChunkLoadError(Chunk chunk, Exception e) { public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing. // Do nothing.
@ -411,36 +435,22 @@ public class DashChunkSource implements ChunkSource {
} }
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
Representation representation, Extractor extractor, DataSource dataSource, Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource,
int trigger) { int trigger) {
int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM;
long indexAnchor = 0;
RangedUri requestUri; RangedUri requestUri;
if (initializationUri != null) { if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge // It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once. // the two requests together to request both at once.
expectedExtractorResult |= Extractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri); requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) { if (requestUri == null) {
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
if (extractor.hasRelativeIndexOffsets()) {
indexAnchor = indexUri.start + indexUri.length;
}
} else {
requestUri = initializationUri; requestUri = initializationUri;
} }
} else { } else {
requestUri = indexUri; requestUri = indexUri;
if (extractor.hasRelativeIndexOffsets()) {
indexAnchor = indexUri.start + indexUri.length;
}
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
} }
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey()); representation.getCacheKey());
return new InitializationChunk(dataSource, dataSpec, trigger, representation.format, extractor);
return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor);
} }
private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource,
@ -451,10 +461,9 @@ public class DashChunkSource implements ChunkSource {
long startTimeUs = segmentIndex.getTimeUs(segmentNum); long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum); long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum);
int absoluteSegmentNum = segmentNum + representationHolder.segmentNumShift;
boolean isLastSegment = !currentManifest.dynamic boolean isLastSegment = !currentManifest.dynamic
&& segmentNum == segmentIndex.getLastSegmentNum(); && segmentNum == segmentIndex.getLastSegmentNum();
int nextAbsoluteSegmentNum = isLastSegment ? -1
: (representationHolder.segmentNumShift + segmentNum + 1);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
@ -470,12 +479,13 @@ public class DashChunkSource implements ChunkSource {
representationHolder.vttHeader = headerBuilder.toString().getBytes(); representationHolder.vttHeader = headerBuilder.toString().getBytes();
representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs; representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs;
} }
return new SingleSampleMediaChunk(dataSource, dataSpec, representation.format, 0, return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader); representation.format, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, null,
null, representationHolder.vttHeader);
} else { } else {
return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger, return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format,
startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, 0,
drmInitData, false, presentationTimeOffsetUs); representationHolder.extractorWrapper, representationHolder.format, drmInitData, true);
} }
} }
@ -559,52 +569,22 @@ public class DashChunkSource implements ChunkSource {
Collections.singletonList(period)); Collections.singletonList(period));
} }
private class InitializationLoadable extends Chunk {
private final Extractor extractor;
private final int expectedExtractorResult;
private final long indexAnchor;
private final Uri uri;
public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, Extractor extractor, int expectedExtractorResult,
long indexAnchor) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor;
this.expectedExtractorResult = expectedExtractorResult;
this.indexAnchor = indexAnchor;
this.uri = dataSpec.uri;
}
@Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
int result = extractor.read(stream, null);
if (result != expectedExtractorResult) {
throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
}
if ((result & Extractor.RESULT_READ_INDEX) != 0) {
representationHolders.get(format.id).segmentIndex =
new DashWrappingSegmentIndex(extractor.getIndex(), uri.toString(), indexAnchor);
}
}
}
private static class RepresentationHolder { private static class RepresentationHolder {
public final Representation representation; public final Representation representation;
public final Extractor extractor; public final ChunkExtractorWrapper extractorWrapper;
public DashSegmentIndex segmentIndex; public DashSegmentIndex segmentIndex;
public int segmentNumShift; public MediaFormat format;
public int segmentNumShift;
public long vttHeaderOffsetUs; public long vttHeaderOffsetUs;
public byte[] vttHeader; public byte[] vttHeader;
public RepresentationHolder(Representation representation, Extractor extractor) { public RepresentationHolder(Representation representation,
ChunkExtractorWrapper extractorWrapper) {
this.representation = representation; this.representation = representation;
this.extractor = extractor; this.extractorWrapper = extractorWrapper;
this.segmentIndex = representation.getIndex(); this.segmentIndex = representation.getIndex();
} }

View file

@ -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 {
@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

View file

@ -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 {
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;

View file

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

View file

@ -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;

View file

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

View file

@ -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;

View file

@ -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 {
/** /**

View file

@ -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;
/** /**
* 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 {
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];
} }

View file

@ -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;

View file

@ -1,51 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.hls;
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
* for the playback of HLS streams.
*/
public abstract class HlsChunk implements Loadable {
protected final DataSource dataSource;
protected final DataSpec dataSpec;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
*/
public HlsChunk(DataSource dataSource, DataSpec dataSpec) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec);
}
public abstract void consume() throws IOException;
public abstract boolean isLoadFinished();
}

View file

@ -17,21 +17,25 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.DataChunk;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ts.AdtsExtractor; import com.google.android.exoplayer.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer.extractor.ts.TsExtractor; import com.google.android.exoplayer.extractor.ts.TsExtractor;
import com.google.android.exoplayer.upstream.Aes128DataSource;
import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.UriUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -117,9 +121,10 @@ public class HlsChunkSource {
private static final float BANDWIDTH_FRACTION = 0.8f; private static final float BANDWIDTH_FRACTION = 0.8f;
private final BufferPool bufferPool; private final BufferPool bufferPool;
private final DataSource upstreamDataSource; private final DataSource dataSource;
private final HlsPlaylistParser playlistParser; private final HlsPlaylistParser playlistParser;
private final Variant[] enabledVariants; private final List<Variant> variants;
private final HlsFormat[] enabledFormats;
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final int adaptiveMode; private final int adaptiveMode;
private final String baseUri; private final String baseUri;
@ -137,11 +142,11 @@ public class HlsChunkSource {
/* package */ boolean live; /* package */ boolean live;
/* package */ long durationUs; /* package */ long durationUs;
private int variantIndex; private int formatIndex;
private DataSource encryptedDataSource;
private Uri encryptionKeyUri; private Uri encryptionKeyUri;
private String encryptedDataSourceIv; private byte[] encryptionKey;
private byte[] encryptedDataSourceSecretKey; private String encryptionIvString;
private byte[] encryptionIv;
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist, public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) { BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
@ -173,7 +178,7 @@ public class HlsChunkSource {
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode, BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
int targetBufferSize, long targetBufferDurationMs, long minBufferDurationToSwitchUpMs, int targetBufferSize, long targetBufferDurationMs, long minBufferDurationToSwitchUpMs,
long maxBufferDurationToSwitchDownMs) { long maxBufferDurationToSwitchDownMs) {
this.upstreamDataSource = dataSource; this.dataSource = dataSource;
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
this.adaptiveMode = adaptiveMode; this.adaptiveMode = adaptiveMode;
this.targetBufferSize = targetBufferSize; this.targetBufferSize = targetBufferSize;
@ -185,30 +190,33 @@ public class HlsChunkSource {
bufferPool = new BufferPool(256 * 1024); bufferPool = new BufferPool(256 * 1024);
if (playlist.type == HlsPlaylist.TYPE_MEDIA) { if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; variants = Collections.singletonList(new Variant(playlistUrl, 0, null, -1, -1));
variantIndices = null;
mediaPlaylists = new HlsMediaPlaylist[1]; mediaPlaylists = new HlsMediaPlaylist[1];
mediaPlaylistBlacklistTimesMs = new long[1]; mediaPlaylistBlacklistTimesMs = new long[1];
lastMediaPlaylistLoadTimesMs = new long[1]; lastMediaPlaylistLoadTimesMs = new long[1];
setMediaPlaylist(0, (HlsMediaPlaylist) playlist); setMediaPlaylist(0, (HlsMediaPlaylist) playlist);
} else { } else {
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER); variants = ((HlsMasterPlaylist) playlist).variants;
enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices); int variantCount = variants.size();
mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; mediaPlaylists = new HlsMediaPlaylist[variantCount];
mediaPlaylistBlacklistTimesMs = new long[enabledVariants.length]; mediaPlaylistBlacklistTimesMs = new long[variantCount];
lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; lastMediaPlaylistLoadTimesMs = new long[variantCount];
} }
enabledFormats = buildEnabledFormats(variants, variantIndices);
int maxWidth = -1; int maxWidth = -1;
int maxHeight = -1; int maxHeight = -1;
// Select the first variant from the master playlist that's enabled. // Select the first variant from the master playlist that's enabled.
long minOriginalVariantIndex = Integer.MAX_VALUE; int minEnabledVariantIndex = Integer.MAX_VALUE;
for (int i = 0; i < enabledVariants.length; i++) { for (int i = 0; i < enabledFormats.length; i++) {
if (enabledVariants[i].index < minOriginalVariantIndex) { if (enabledFormats[i].variantIndex < minEnabledVariantIndex) {
minOriginalVariantIndex = enabledVariants[i].index; minEnabledVariantIndex = enabledFormats[i].variantIndex;
variantIndex = i; formatIndex = i;
} }
maxWidth = Math.max(enabledVariants[i].width, maxWidth); maxWidth = Math.max(enabledFormats[i].width, maxWidth);
maxHeight = Math.max(enabledVariants[i].height, maxHeight); maxHeight = Math.max(enabledFormats[i].height, maxHeight);
} }
// TODO: We should allow the default values to be passed through the constructor. // TODO: We should allow the default values to be passed through the constructor.
this.maxWidth = maxWidth > 0 ? maxWidth : 1920; this.maxWidth = maxWidth > 0 ? maxWidth : 1920;
@ -232,7 +240,7 @@ public class HlsChunkSource {
} }
/** /**
* Returns the next {@link HlsChunk} that should be loaded. * Returns the next {@link Chunk} that should be loaded.
* *
* @param previousTsChunk The previously loaded chunk that the next chunk should follow. * @param previousTsChunk The previously loaded chunk that the next chunk should follow.
* @param seekPositionUs If there is no previous chunk, this parameter must specify the seek * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
@ -240,7 +248,7 @@ public class HlsChunkSource {
* @param playbackPositionUs The current playback position. * @param playbackPositionUs The current playback position.
* @return The next chunk to load. * @return The next chunk to load.
*/ */
public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) { long playbackPositionUs) {
if (previousTsChunk != null && (previousTsChunk.isLastChunk if (previousTsChunk != null && (previousTsChunk.isLastChunk
|| previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs) || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)
@ -249,24 +257,27 @@ public class HlsChunkSource {
return null; return null;
} }
int nextVariantIndex = variantIndex; int nextFormatIndex;
boolean switchingVariant = false; boolean switchingVariant;
boolean switchingVariantSpliced = false; boolean switchingVariantSpliced;
if (adaptiveMode == ADAPTIVE_MODE_NONE) { if (adaptiveMode == ADAPTIVE_MODE_NONE) {
// Do nothing. nextFormatIndex = formatIndex;
switchingVariant = false;
switchingVariantSpliced = false;
} else { } else {
nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); nextFormatIndex = getNextFormatIndex(previousTsChunk, playbackPositionUs);
switchingVariant = nextVariantIndex != variantIndex; switchingVariant = nextFormatIndex != formatIndex;
switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE; switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE;
} }
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[nextVariantIndex]; int variantIndex = enabledFormats[nextFormatIndex].variantIndex;
HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
if (mediaPlaylist == null) { if (mediaPlaylist == null) {
// We don't have the media playlist for the next variant. Request it now. // We don't have the media playlist for the next variant. Request it now.
return newMediaPlaylistChunk(nextVariantIndex); return newMediaPlaylistChunk(variantIndex);
} }
variantIndex = nextVariantIndex; formatIndex = nextFormatIndex;
int chunkMediaSequence = 0; int chunkMediaSequence = 0;
boolean liveDiscontinuity = false; boolean liveDiscontinuity = false;
if (live) { if (live) {
@ -309,18 +320,17 @@ public class HlsChunkSource {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) { if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed. // Encryption is specified and the key has changed.
HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV); Chunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
return toReturn; return toReturn;
} }
if (!Util.areEqual(segment.encryptionIV, encryptedDataSourceIv)) { if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) {
initEncryptedDataSource(keyUri, segment.encryptionIV, encryptedDataSourceSecretKey); setEncryptionData(keyUri, segment.encryptionIV, encryptionKey);
} }
} else { } else {
clearEncryptedDataSource(); clearEncryptionData();
} }
// Configure the data source and spec for the chunk. // Configure the data source and spec for the chunk.
DataSource dataSource = encryptedDataSource != null ? encryptedDataSource : upstreamDataSource;
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null); null);
@ -348,22 +358,44 @@ public class HlsChunkSource {
: new TsExtractor(startTimeUs); : new TsExtractor(startTimeUs);
extractorWrapper = new HlsExtractorWrapper(bufferPool, extractor, switchingVariantSpliced); extractorWrapper = new HlsExtractorWrapper(bufferPool, extractor, switchingVariantSpliced);
} else { } else {
extractorWrapper = previousTsChunk.extractor; extractorWrapper = previousTsChunk.extractorWrapper;
} }
return new TsChunk(dataSource, dataSpec, extractorWrapper, enabledVariants[variantIndex].index, return new TsChunk(dataSource, dataSpec, Chunk.TRIGGER_UNSPECIFIED, enabledFormats[formatIndex],
startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk); startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk, extractorWrapper, encryptionKey,
encryptionIv);
} }
/** /**
* Invoked when an error occurs loading a chunk. * Invoked when the {@link HlsSampleSource} has finished loading a chunk obtained from this
* source.
* *
* @param chunk The chunk whose load failed. * @param chunk The chunk whose load has been completed.
* @param e The failure. */
public void onChunkLoadCompleted(Chunk chunk) {
if (chunk instanceof MediaPlaylistChunk) {
MediaPlaylistChunk mediaPlaylistChunk = (MediaPlaylistChunk) chunk;
scratchSpace = mediaPlaylistChunk.getDataHolder();
setMediaPlaylist(mediaPlaylistChunk.variantIndex, mediaPlaylistChunk.getResult());
} else if (chunk instanceof EncryptionKeyChunk) {
EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;
scratchSpace = encryptionKeyChunk.getDataHolder();
setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv,
encryptionKeyChunk.getResult());
}
}
/**
* Invoked when the {@link HlsSampleSource} encounters an error loading a chunk obtained from
* this source.
*
* @param chunk The chunk whose load encountered the error.
* @param e The error.
* @return True if the error was handled by the source. False otherwise. * @return True if the error was handled by the source. False otherwise.
*/ */
public boolean onLoadError(HlsChunk chunk, IOException e) { public boolean onChunkLoadError(Chunk chunk, IOException e) {
if ((chunk instanceof MediaPlaylistChunk) && (e instanceof InvalidResponseCodeException)) { if (chunk.bytesLoaded() == 0 && (chunk instanceof MediaPlaylistChunk)
&& (e instanceof InvalidResponseCodeException)) {
InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e;
int responseCode = responseCodeException.responseCode; int responseCode = responseCodeException.responseCode;
if (responseCode == 404 || responseCode == 410) { if (responseCode == 404 || responseCode == 410) {
@ -386,49 +418,52 @@ public class HlsChunkSource {
return false; return false;
} }
private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { private int getNextFormatIndex(TsChunk previousTsChunk, long playbackPositionUs) {
clearStaleBlacklistedPlaylists(); clearStaleBlacklistedPlaylists();
if (previousTsChunk == null) { if (previousTsChunk == null) {
// Don't consider switching if we don't have a previous chunk. // Don't consider switching if we don't have a previous chunk.
return variantIndex; return formatIndex;
} }
long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) { if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) {
// Don't consider switching if we don't have a bandwidth estimate. // Don't consider switching if we don't have a bandwidth estimate.
return variantIndex; return formatIndex;
} }
int idealVariantIndex = getVariantIndexForBandwdith( int idealFormatIndex = getFormatIndexForBandwidth(
(int) (bitrateEstimate * BANDWIDTH_FRACTION)); (int) (bitrateEstimate * BANDWIDTH_FRACTION));
if (idealVariantIndex == variantIndex) { if (idealFormatIndex == formatIndex) {
// We're already using the ideal variant. // We're already using the ideal format.
return variantIndex; return formatIndex;
} }
// We're not using the ideal variant for the available bandwidth, but only switch if the // We're not using the ideal format for the available bandwidth, but only switch if the
// conditions are appropriate. // conditions are appropriate.
long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs; : previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs; long bufferedUs = bufferedPositionUs - playbackPositionUs;
if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0 if (mediaPlaylistBlacklistTimesMs[formatIndex] != 0
|| (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) || (idealFormatIndex > formatIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
|| (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { || (idealFormatIndex < formatIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
// Switch variant. // Switch format.
return idealVariantIndex; return idealFormatIndex;
} }
// Stick with the current variant for now. // Stick with the current format for now.
return variantIndex; return formatIndex;
} }
private int getVariantIndexForBandwdith(int bandwidth) { private int getFormatIndexForBandwidth(int bitrate) {
int lowestQualityEnabledVariant = 0; int lowestQualityEnabledFormatIndex = -1;
for (int i = 0; i < enabledVariants.length; i++) { for (int i = 0; i < enabledFormats.length; i++) {
if (mediaPlaylistBlacklistTimesMs[i] == 0) { int variantIndex = enabledFormats[i].variantIndex;
if (enabledVariants[i].bandwidth <= bandwidth) { if (mediaPlaylistBlacklistTimesMs[variantIndex] == 0) {
if (enabledFormats[i].bitrate <= bitrate) {
return i; return i;
} }
lowestQualityEnabledVariant = i; lowestQualityEnabledFormatIndex = i;
} }
} }
return lowestQualityEnabledVariant; // At least one format should always be enabled.
Assertions.checkState(lowestQualityEnabledFormatIndex != -1);
return lowestQualityEnabledFormatIndex;
} }
private boolean shouldRerequestMediaPlaylist(int variantIndex) { private boolean shouldRerequestMediaPlaylist(int variantIndex) {
@ -447,19 +482,19 @@ public class HlsChunkSource {
} }
private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) {
Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, enabledVariants[variantIndex].url); Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants.get(variantIndex).url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null,
DataSpec.FLAG_ALLOW_GZIP); DataSpec.FLAG_ALLOW_GZIP);
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, return new MediaPlaylistChunk(dataSource, dataSpec, scratchSpace, playlistParser, variantIndex,
mediaPlaylistUri.toString()); mediaPlaylistUri.toString());
} }
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP);
return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv); return new EncryptionKeyChunk(dataSource, dataSpec, scratchSpace, iv);
} }
/* package */ void initEncryptedDataSource(Uri keyUri, String iv, byte[] secretKey) { /* package */ void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) {
String trimmedIv; String trimmedIv;
if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
trimmedIv = iv.substring(2); trimmedIv = iv.substring(2);
@ -473,17 +508,17 @@ public class HlsChunkSource {
System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+ offset, ivData.length - offset); + offset, ivData.length - offset);
encryptedDataSource = new Aes128DataSource(upstreamDataSource, secretKey, ivDataWithPadding);
encryptionKeyUri = keyUri; encryptionKeyUri = keyUri;
encryptedDataSourceIv = iv; encryptionKey = secretKey;
encryptedDataSourceSecretKey = secretKey; encryptionIvString = iv;
encryptionIv = ivDataWithPadding;
} }
private void clearEncryptedDataSource() { private void clearEncryptionData() {
encryptionKeyUri = null; encryptionKeyUri = null;
encryptedDataSource = null; encryptionKey = null;
encryptedDataSourceIv = null; encryptionIvString = null;
encryptedDataSourceSecretKey = null; encryptionIv = null;
} }
/* package */ void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { /* package */ void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) {
@ -493,16 +528,15 @@ public class HlsChunkSource {
durationUs = mediaPlaylist.durationUs; durationUs = mediaPlaylist.durationUs;
} }
private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) { private static HlsFormat[] buildEnabledFormats(List<Variant> variants, int[] variantIndices) {
List<Variant> masterVariants = masterPlaylist.variants;
ArrayList<Variant> enabledVariants = new ArrayList<Variant>(); ArrayList<Variant> enabledVariants = new ArrayList<Variant>();
if (variantIndices != null) { if (variantIndices != null) {
for (int i = 0; i < variantIndices.length; i++) { for (int i = 0; i < variantIndices.length; i++) {
enabledVariants.add(masterVariants.get(variantIndices[i])); enabledVariants.add(variants.get(variantIndices[i]));
} }
} else { } else {
// If variantIndices is null then all variants are initially considered. // If variantIndices is null then all variants are initially considered.
enabledVariants.addAll(masterVariants); enabledVariants.addAll(variants);
} }
ArrayList<Variant> definiteVideoVariants = new ArrayList<Variant>(); ArrayList<Variant> definiteVideoVariants = new ArrayList<Variant>();
@ -529,20 +563,26 @@ public class HlsChunkSource {
// Leave the enabled variants unchanged. They're likely either all video or all audio. // Leave the enabled variants unchanged. They're likely either all video or all audio.
} }
Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator()); HlsFormat[] enabledFormats = new HlsFormat[enabledVariants.size()];
for (int i = 0; i < enabledFormats.length; i++) {
Variant variant = enabledVariants.get(i);
int variantIndex = variants.indexOf(variant);
enabledFormats[i] = new HlsFormat(Integer.toString(variantIndex), variant.width,
variant.height, variant.bitrate, variant.codecs, variantIndex);
}
Variant[] enabledVariantsArray = new Variant[enabledVariants.size()]; Arrays.sort(enabledFormats, new Format.DecreasingBandwidthComparator());
enabledVariants.toArray(enabledVariantsArray); return enabledFormats;
return enabledVariantsArray;
} }
private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) {
String[] codecs = variant.codecs; String codecs = variant.codecs;
if (codecs == null) { if (TextUtils.isEmpty(codecs)) {
return false; return false;
} }
for (int i = 0; i < codecs.length; i++) { String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
if (codecs[i].startsWith(prefix)) { for (int i = 0; i < codecArray.length; i++) {
if (codecArray[i].startsWith(prefix)) {
return true; return true;
} }
} }
@ -568,47 +608,67 @@ public class HlsChunkSource {
} }
} }
private class MediaPlaylistChunk extends DataChunk { private static class MediaPlaylistChunk extends DataChunk {
@SuppressWarnings("hiding") public final int variantIndex;
/* package */ final int variantIndex;
private final HlsPlaylistParser playlistParser;
private final String playlistUrl; private final String playlistUrl;
public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, private HlsMediaPlaylist result;
String playlistUrl) {
super(dataSource, dataSpec, scratchSpace); public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace,
HlsPlaylistParser playlistParser, int variantIndex, String playlistUrl) {
super(dataSource, dataSpec, Chunk.TYPE_MANIFEST, Chunk.TRIGGER_UNSPECIFIED, null,
scratchSpace);
this.variantIndex = variantIndex; this.variantIndex = variantIndex;
this.playlistParser = playlistParser;
this.playlistUrl = playlistUrl; this.playlistUrl = playlistUrl;
} }
@Override @Override
protected void consume(byte[] data, int limit) throws IOException { protected void consume(byte[] data, int limit) throws IOException {
HlsPlaylist playlist = playlistParser.parse(playlistUrl, result = (HlsMediaPlaylist) playlistParser.parse(playlistUrl,
new ByteArrayInputStream(data, 0, limit)); new ByteArrayInputStream(data, 0, limit));
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); }
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
setMediaPlaylist(variantIndex, mediaPlaylist); public HlsMediaPlaylist getResult() {
// Recycle the allocation. return result;
scratchSpace = data;
} }
} }
private class EncryptionKeyChunk extends DataChunk { private static class EncryptionKeyChunk extends DataChunk {
private final String iv; public final String iv;
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) { private byte[] result;
super(dataSource, dataSpec, scratchSpace);
public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace,
String iv) {
super(dataSource, dataSpec, Chunk.TYPE_DRM, Chunk.TRIGGER_UNSPECIFIED, null, scratchSpace);
this.iv = iv; this.iv = iv;
} }
@Override @Override
protected void consume(byte[] data, int limit) throws IOException { protected void consume(byte[] data, int limit) throws IOException {
initEncryptedDataSource(dataSpec.uri, iv, Arrays.copyOf(data, limit)); result = Arrays.copyOf(data, limit);
// Recycle the allocation. }
scratchSpace = data;
public byte[] getResult() {
return result;
}
}
private static final class HlsFormat extends Format {
public final int variantIndex;
public HlsFormat(String id, int width, int height, int bitrate, String codecs,
int variantIndex) {
super(id, MimeTypes.APPLICATION_M3U8, width, height, -1, -1, bitrate, null, codecs);
this.variantIndex = variantIndex;
} }
} }

View file

@ -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
// 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
} }
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;

View file

@ -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 {
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 {
@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 {
@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 {
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 {
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;
} }

View file

@ -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.
*/
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 final boolean isEncrypted;
private volatile boolean loadFinished;
private int bytesLoaded;
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 {
@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);
}
} }

View file

@ -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;

View file

@ -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;
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 {
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 {
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 {
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 {
} }
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 {
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;
} }
@ -312,6 +317,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
: (manifestFetcher != null ? manifestFetcher.getError() : null); : (manifestFetcher != null ? manifestFetcher.getError() : null);
} }
@Override
public void onChunkLoadCompleted(Chunk chunk) {
// Do nothing.
}
@Override @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 {
} }
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) {

View file

@ -1,77 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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();
}

View file

@ -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.
*/ */
public int getAllocatedSize(); int getAllocatedSize();
/**
* Returns the length of each buffer provided by the allocator.
*/
int getBufferLength();
} }

View file

@ -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 {
} }
@Override @Override
public synchronized int getAllocatedSize() { public synchronized byte[] allocateBuffer() {
return allocatedBufferCount * bufferLength; allocatedCount++;
return recycledCount > 0 ? recycledBuffers[--recycledCount] : new byte[bufferLength];
}
@Override
public synchronized void releaseBuffer(byte[] buffer) {
// Weak sanity check that the buffer probably originated from this pool.
Assertions.checkArgument(buffer.length == bufferLength);
allocatedCount--;
if (recycledCount == recycledBuffers.length) {
recycledBuffers = Arrays.copyOf(recycledBuffers, recycledBuffers.length * 2);
}
recycledBuffers[recycledCount++] = buffer;
// Wake up threads waiting for the allocated size to drop.
notifyAll();
} }
@Override @Override
public synchronized void trim(int targetSize) { public synchronized void trim(int targetSize) {
int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength; int targetBufferCount = (targetSize + bufferLength - 1) / bufferLength;
int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedBufferCount); int targetRecycledBufferCount = Math.max(0, targetBufferCount - allocatedCount);
if (targetRecycledBufferCount < recycledBufferCount) { if (targetRecycledBufferCount < recycledCount) {
Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledBufferCount, null); Arrays.fill(recycledBuffers, targetRecycledBufferCount, recycledCount, null);
recycledBufferCount = targetRecycledBufferCount; recycledCount = targetRecycledBufferCount;
} }
} }
@Override @Override
public synchronized Allocation allocate(int size) { public synchronized int getAllocatedSize() {
return new AllocationImpl(allocate(size, null)); return allocatedCount * bufferLength;
} }
/** @Override
* Allocates byte arrays whose combined length is at least {@code size}. public int getBufferLength() {
* <p> return bufferLength;
* 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;
}
/**
* Obtain a single buffer directly from the pool.
* <p>
* When the caller has finished with the buffer, it should be returned to the pool by calling
* {@link #releaseDirect(byte[])}.
*
* @return The allocated buffer.
*/
public synchronized byte[] allocateDirect() {
allocatedBufferCount++;
return nextBuffer();
}
/**
* Return a single buffer to the pool.
*
* @param buffer The buffer being returned.
*/
public synchronized void releaseDirect(byte[] buffer) {
// Weak sanity check that the buffer probably originated from this pool.
Assertions.checkArgument(buffer.length == bufferLength);
allocatedBufferCount--;
ensureRecycledBufferCapacity(recycledBufferCount + 1);
recycledBuffers[recycledBufferCount++] = buffer;
} }
/** /**
@ -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;
}
}
}
} }

View file

@ -1,150 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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;
}
}

View file

@ -1,83 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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);
}
}

View file

@ -1,390 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Loads data from a {@link DataSource} into an in-memory {@link Allocation}. The loaded data
* can be consumed by treating the instance as a non-blocking {@link NonBlockingInputStream}.
*/
public final class DataSourceStream implements Loadable, NonBlockingInputStream {
/**
* Thrown when an error is encountered trying to load data into a {@link DataSourceStream}.
*/
public static class DataSourceStreamLoadException extends IOException {
public DataSourceStreamLoadException(IOException cause) {
super(cause);
}
}
private static final int CHUNKED_ALLOCATION_INCREMENT = 256 * 1024;
private final DataSource dataSource;
private final DataSpec dataSpec;
private final Allocator allocator;
private final ReadHead readHead;
/** Whether {@link #allocation}'s capacity is fixed. If true, the allocation is not resized. */
private final boolean isAllocationFixedSize;
private final int allocationSize;
private Allocation allocation;
private volatile boolean loadCanceled;
private volatile long loadPosition;
private volatile long resolvedLength;
private int writeFragmentIndex;
private int writeFragmentOffset;
private int writeFragmentRemainingLength;
/**
* Constructs an instance whose allocation grows to contain all of the data specified by the
* {@code dataSpec}.
*
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
* {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
* the length resolved by {@code dataSource.open(dataSpec)} must not exceed
* {@link Integer#MAX_VALUE}.
* @param allocator Used to obtain an {@link Allocation} for holding the data.
*/
public DataSourceStream(DataSource dataSource, DataSpec dataSpec, Allocator allocator) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = dataSource;
this.dataSpec = dataSpec;
this.allocator = allocator;
resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead();
isAllocationFixedSize = false;
allocationSize = 0;
}
/**
* Constructs an instance whose allocation is of a fixed size, which may be smaller than the data
* specified by the {@code dataSpec}.
* <p>
* The allocation size determines how far ahead loading can proceed relative to the current
* reading position.
*
* @param dataSource The source form which the data should be loaded.
* @param dataSpec Defines the data to be loaded.
* @param allocator Used to obtain an {@link Allocation} for holding the data.
* @param allocationSize The minimum size for a fixed-size allocation that will hold the data
* loaded from {@code dataSource}.
*/
public DataSourceStream(
DataSource dataSource, DataSpec dataSpec, Allocator allocator, int allocationSize) {
Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
this.dataSource = dataSource;
this.dataSpec = dataSpec;
this.allocator = allocator;
this.allocationSize = allocationSize;
resolvedLength = C.LENGTH_UNBOUNDED;
readHead = new ReadHead();
isAllocationFixedSize = true;
}
/**
* Resets the read position to the start of the data.
*
* @throws UnsupportedOperationException Thrown if the allocation size is fixed.
*/
public void resetReadPosition() {
if (isAllocationFixedSize) {
throw new UnsupportedOperationException(
"The read position cannot be reset when using a fixed allocation");
}
readHead.reset();
}
/**
* Returns the current read position for data being read out of the source.
*
* @return The current read position.
*/
public long getReadPosition() {
return readHead.position;
}
/**
* Returns the number of bytes of data that have been loaded.
*
* @return The number of bytes of data that have been loaded.
*/
public long getLoadPosition() {
return loadPosition;
}
/**
* Returns the length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has
* yet to be determined.
*
* @return The length of the stream in bytes, or {@value C#LENGTH_UNBOUNDED} if the length has
* yet to be determined.
*/
public long getLength() {
return resolvedLength != C.LENGTH_UNBOUNDED ? resolvedLength : dataSpec.length;
}
/**
* Whether the stream has finished loading.
*
* @return True if the stream has finished loading. False otherwise.
*/
public boolean isLoadFinished() {
return resolvedLength != C.LENGTH_UNBOUNDED && loadPosition == resolvedLength;
}
// {@link NonBlockingInputStream} implementation.
@Override
public long getAvailableByteCount() {
return loadPosition - readHead.position;
}
@Override
public boolean isEndOfStream() {
return resolvedLength != C.LENGTH_UNBOUNDED && readHead.position == resolvedLength;
}
@Override
public void close() {
if (allocation != null) {
allocation.release();
allocation = null;
}
}
@Override
public int skip(int skipLength) {
return read(null, null, 0, readHead, skipLength);
}
@Override
public int read(ByteBuffer target1, int readLength) {
return read(target1, null, 0, readHead, readLength);
}
@Override
public int read(byte[] target, int offset, int readLength) {
return read(null, target, offset, readHead, readLength);
}
/**
* Reads data to either a target {@link ByteBuffer}, or to a target byte array at a specified
* offset. The {@code readHead} is updated to reflect the read that was performed.
*/
private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset,
ReadHead readHead, int readLength) {
if (isEndOfStream()) {
return -1;
}
int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength);
if (bytesToRead == 0) {
return 0;
}
if (readHead.position == 0) {
readHead.fragmentIndex = 0;
readHead.fragmentOffset = allocation.getFragmentOffset(0);
readHead.fragmentRemaining = allocation.getFragmentLength(0);
}
int bytesRead = 0;
byte[][] buffers = allocation.getBuffers();
while (bytesRead < bytesToRead) {
if (readHead.fragmentRemaining == 0) {
if (readHead.fragmentIndex == buffers.length - 1) {
Assertions.checkState(isAllocationFixedSize);
readHead.fragmentIndex = 0;
} else {
readHead.fragmentIndex++;
}
readHead.fragmentOffset = allocation.getFragmentOffset(readHead.fragmentIndex);
readHead.fragmentRemaining = allocation.getFragmentLength(readHead.fragmentIndex);
}
int bufferReadLength = Math.min(readHead.fragmentRemaining, bytesToRead - bytesRead);
if (target != null) {
target.put(buffers[readHead.fragmentIndex], readHead.fragmentOffset, bufferReadLength);
} else if (targetArray != null) {
System.arraycopy(buffers[readHead.fragmentIndex], readHead.fragmentOffset, targetArray,
targetArrayOffset, bufferReadLength);
targetArrayOffset += bufferReadLength;
}
readHead.position += bufferReadLength;
bytesRead += bufferReadLength;
readHead.fragmentOffset += bufferReadLength;
readHead.fragmentRemaining -= bufferReadLength;
}
if (isAllocationFixedSize) {
synchronized (readHead) {
// Notify load() of the updated position so it can resume.
readHead.notify();
}
}
return bytesRead;
}
// {@link Loadable} implementation.
@Override
public void cancelLoad() {
loadCanceled = true;
}
@Override
public boolean isLoadCanceled() {
return loadCanceled;
}
@Override
@SuppressWarnings("NonAtomicVolatileUpdate")
public void load() throws IOException, InterruptedException {
if (loadCanceled || isLoadFinished()) {
// The load was canceled, or is already complete.
return;
}
try {
DataSpec loadDataSpec;
if (loadPosition == 0 && resolvedLength == C.LENGTH_UNBOUNDED) {
loadDataSpec = dataSpec;
long resolvedLength = dataSource.open(loadDataSpec);
if (!isAllocationFixedSize && resolvedLength > Integer.MAX_VALUE) {
throw new DataSourceStreamLoadException(
new UnexpectedLengthException(dataSpec.length, resolvedLength));
}
this.resolvedLength = resolvedLength;
} else {
long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED
? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED;
loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition,
remainingLength, dataSpec.key, dataSpec.flags);
dataSource.open(loadDataSpec);
}
if (allocation == null) {
if (isAllocationFixedSize) {
allocation = allocator.allocate(allocationSize);
} else {
int initialAllocationSize = resolvedLength != C.LENGTH_UNBOUNDED
? (int) resolvedLength : CHUNKED_ALLOCATION_INCREMENT;
allocation = allocator.allocate(initialAllocationSize);
}
}
int allocationCapacity = allocation.capacity();
if (loadPosition == 0) {
writeFragmentIndex = 0;
writeFragmentOffset = allocation.getFragmentOffset(0);
writeFragmentRemainingLength = allocation.getFragmentLength(0);
}
int read = Integer.MAX_VALUE;
byte[][] buffers = allocation.getBuffers();
while (!loadCanceled && read > 0 && maybeMoreToLoad()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
int bytesToWrite = getBytesToWrite();
read = dataSource.read(buffers[writeFragmentIndex], writeFragmentOffset, bytesToWrite);
if (read > 0) {
loadPosition += read;
writeFragmentOffset += read;
writeFragmentRemainingLength -= read;
if (writeFragmentRemainingLength == 0 && maybeMoreToLoad()) {
writeFragmentIndex++;
if (writeFragmentIndex == buffers.length) {
if (isAllocationFixedSize) {
// Wrap back to the first fragment.
writeFragmentIndex = 0;
} else {
// Grow the allocation.
allocation.ensureCapacity(allocationCapacity + CHUNKED_ALLOCATION_INCREMENT);
allocationCapacity = allocation.capacity();
buffers = allocation.getBuffers();
}
}
writeFragmentOffset = allocation.getFragmentOffset(writeFragmentIndex);
writeFragmentRemainingLength = allocation.getFragmentLength(writeFragmentIndex);
}
} else if (resolvedLength == C.LENGTH_UNBOUNDED) {
resolvedLength = loadPosition;
} else if (resolvedLength != loadPosition) {
throw new DataSourceStreamLoadException(
new UnexpectedLengthException(resolvedLength, loadPosition));
}
}
} finally {
Util.closeQuietly(dataSource);
}
}
/**
* Returns the number of bytes that can be written to the current fragment, blocking until the
* reader has consumed data if the allocation has a fixed size and is full.
*/
private int getBytesToWrite() throws InterruptedException {
if (!isAllocationFixedSize) {
return writeFragmentRemainingLength;
}
synchronized (readHead) {
while (loadPosition == readHead.position + allocation.capacity()) {
readHead.wait();
}
}
return Math.min(writeFragmentRemainingLength,
allocation.capacity() - (int) (loadPosition - readHead.position));
}
private boolean maybeMoreToLoad() {
return resolvedLength == C.LENGTH_UNBOUNDED || loadPosition < resolvedLength;
}
private static class ReadHead {
private int position;
private int fragmentIndex;
private int fragmentOffset;
private int fragmentRemaining;
public void reset() {
position = 0;
fragmentIndex = 0;
fragmentOffset = 0;
fragmentRemaining = 0;
}
}
}

View file

@ -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 {
} }
@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>

View file

@ -1,79 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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();
}

View file

@ -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;
@ -460,6 +461,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.

View file

@ -1,350 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.chunk.parser.webm;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.ByteArrayNonBlockingInputStream;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import junit.framework.TestCase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Tests {@link DefaultEbmlReader}.
*/
public class DefaultEbmlReaderTest extends TestCase {
private final EventCapturingEbmlEventHandler eventHandler =
new EventCapturingEbmlEventHandler();
public void testNothing() {
NonBlockingInputStream input = createTestInputStream();
assertNoEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM);
}
public void testMasterElement() {
NonBlockingInputStream input =
createTestInputStream(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onMasterElementStart(EventCapturingEbmlEventHandler.ID_EBML, 0, 5, 4);
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE_READ_VERSION, 1);
expected.onMasterElementEnd(EventCapturingEbmlEventHandler.ID_EBML);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testMasterElementEmpty() {
NonBlockingInputStream input = createTestInputStream(0x18, 0x53, 0x80, 0x67, 0x80);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onMasterElementStart(EventCapturingEbmlEventHandler.ID_SEGMENT, 0, 5, 0);
expected.onMasterElementEnd(EventCapturingEbmlEventHandler.ID_SEGMENT);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testUnsignedIntegerElement() {
// 0xFE is chosen because for signed integers it should be interpreted as -2
NonBlockingInputStream input = createTestInputStream(0x42, 0xF7, 0x81, 0xFE);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, 254);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testUnsignedIntegerElementLarge() {
NonBlockingInputStream input =
createTestInputStream(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, Long.MAX_VALUE);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testUnsignedIntegerElementTooLargeBecomesNegative() {
NonBlockingInputStream input =
createTestInputStream(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, -1);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testStringElement() {
NonBlockingInputStream input =
createTestInputStream(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onStringElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE, "Abc123");
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testStringElementEmpty() {
NonBlockingInputStream input = createTestInputStream(0x42, 0x82, 0x80);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onStringElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE, "");
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testFloatElementThreeBytes() {
try {
eventHandler.read(createTestInputStream(0x44, 0x89, 0x83, 0x3F, 0x80, 0x00));
fail();
} catch (IllegalStateException exception) {
// Expected
}
assertNoEvents();
}
public void testFloatElementFourBytes() {
NonBlockingInputStream input =
createTestInputStream(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onFloatElement(EventCapturingEbmlEventHandler.ID_DURATION, 1.0);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testFloatElementEightBytes() {
NonBlockingInputStream input =
createTestInputStream(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.onFloatElement(EventCapturingEbmlEventHandler.ID_DURATION, -2.0);
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementReadBytes() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_BYTES;
NonBlockingInputStream input =
createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_BYTES;
expected.onBinaryElement(
EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 8,
createTestInputStream(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementReadVarint() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_VARINT;
NonBlockingInputStream input = createTestInputStream(0xA3, 0x82, 0x40, 0x2A);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_VARINT;
expected.onBinaryElement(
EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 0,
createTestInputStream(0x40, 0x2A));
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementSkipBytes() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_SKIP_BYTES;
NonBlockingInputStream input =
createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08);
EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler();
expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_SKIP_BYTES;
expected.onBinaryElement(
EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 8,
createTestInputStream(0, 0, 0, 0, 0, 0, 0, 0));
assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events);
}
public void testBinaryElementDoNothing() {
eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_DO_NOTHING;
try {
eventHandler.read(
createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08));
fail();
} catch (IllegalStateException exception) {
// Expected
}
assertNoEvents();
}
public void testBinaryElementNotEnoughBytes() {
NonBlockingInputStream input = createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03);
assertNoEvents(input, EbmlReader.READ_RESULT_NEED_MORE_DATA);
}
public void testUnknownElement() {
NonBlockingInputStream input = createTestInputStream(0xEC, 0x81, 0x00);
assertNoEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM);
}
/**
* Helper to build a {@link ByteArrayNonBlockingInputStream} quickly from zero or more
* integer arguments.
*
* <p>Each argument must be able to cast to a byte value.
*
* @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}
* @return A {@link ByteArrayNonBlockingInputStream} containing the given byte values
*/
private NonBlockingInputStream createTestInputStream(int... data) {
byte[] bytes = new byte[data.length];
for (int i = 0; i < data.length; i++) {
bytes[i] = (byte) data[i];
}
return new ByteArrayNonBlockingInputStream(bytes);
}
private void assertReads(NonBlockingInputStream input, int continues, int finalResult) {
for (int i = 0; i < continues; i++) {
assertEquals(EbmlReader.READ_RESULT_CONTINUE, eventHandler.read(input));
}
assertEquals(finalResult, eventHandler.read(input));
}
private void assertNoEvents() {
assertEvents(Collections.<String>emptyList());
}
private void assertEvents(List<String> events) {
assertEquals(events.size(), eventHandler.events.size());
for (int i = 0; i < events.size(); i++) {
assertEquals(events.get(i), eventHandler.events.get(i));
}
}
private void assertNoEvents(NonBlockingInputStream input, int finalResult) {
assertReads(input, 0, finalResult);
assertNoEvents();
}
private void assertEvents(NonBlockingInputStream input, int finalResult, List<String> events) {
assertReads(input, events.size(), finalResult);
assertEvents(events);
}
/**
* An {@link EbmlEventHandler} which captures all event callbacks made by
* {@link DefaultEbmlReader} for testing purposes.
*/
private static final class EventCapturingEbmlEventHandler implements EbmlEventHandler {
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_DURATION = 0x4489;
private static final int ID_SIMPLE_BLOCK = 0xA3;
// Various ways to handle things in onBinaryElement()
private static final int HANDLER_DO_NOTHING = 0;
private static final int HANDLER_READ_BYTES = 1;
private static final int HANDLER_READ_VARINT = 2;
private static final int HANDLER_SKIP_BYTES = 3;
private final EbmlReader reader = new DefaultEbmlReader();
private final List<String> events = new ArrayList<String>();
private int binaryElementHandler;
private EventCapturingEbmlEventHandler() {
reader.setEventHandler(this);
}
private int read(NonBlockingInputStream inputStream) {
try {
return reader.read(inputStream);
} catch (ParserException e) {
// should never happen.
fail();
return -1;
}
}
@Override
public int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
return EbmlReader.TYPE_STRING;
case ID_SIMPLE_BLOCK:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
@Override
public void onMasterElementStart(
int id, long elementOffset, int headerSize, long contentsSize) {
events.add(formatEvent(id, "start elementOffset=" + elementOffset
+ " headerSize=" + headerSize + " contentsSize=" + contentsSize));
}
@Override
public void onMasterElementEnd(int id) {
events.add(formatEvent(id, "end"));
}
@Override
public void onIntegerElement(int id, long value) {
events.add(formatEvent(id, "integer=" + String.valueOf(value)));
}
@Override
public void onFloatElement(int id, double value) {
events.add(formatEvent(id, "float=" + String.valueOf(value)));
}
@Override
public void onStringElement(int id, String value) {
events.add(formatEvent(id, "string=" + value));
}
@Override
public boolean onBinaryElement(
int id, long elementOffset, int headerSize, int contentsSize,
NonBlockingInputStream inputStream) {
switch (binaryElementHandler) {
case HANDLER_READ_BYTES:
byte[] bytes = new byte[contentsSize];
reader.readBytes(inputStream, bytes, contentsSize);
events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes)));
break;
case HANDLER_READ_VARINT:
long value = reader.readVarint(inputStream);
events.add(formatEvent(id, "varint=" + String.valueOf(value)));
break;
case HANDLER_SKIP_BYTES:
reader.skipBytes(inputStream, contentsSize);
events.add(formatEvent(id, "skipped " + contentsSize + " byte(s)"));
break;
case HANDLER_DO_NOTHING:
default:
// pass
}
return true;
}
private static String formatEvent(int id, String event) {
return "[" + Integer.toHexString(id) + "] " + event;
}
}
}

View file

@ -1,717 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.chunk.parser.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.chunk.parser.SegmentIndex;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.upstream.ByteArrayNonBlockingInputStream;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.MimeTypes;
import android.test.InstrumentationTestCase;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.UUID;
public class WebmExtractorTest extends InstrumentationTestCase {
private static final int INFO_ELEMENT_BYTE_SIZE = 31;
private static final int TRACKS_ELEMENT_BYTE_SIZE = 48;
private static final int CUES_ELEMENT_BYTE_SIZE = 12;
private static final int CUE_POINT_ELEMENT_BYTE_SIZE = 31;
private static final int DEFAULT_TIMECODE_SCALE = 1000000;
private static final long TEST_DURATION_US = 9920000L;
private static final int TEST_WIDTH = 1280;
private static final int TEST_HEIGHT = 720;
private static final int TEST_CHANNEL_COUNT = 1;
private static final int TEST_SAMPLE_RATE = 48000;
private static final long TEST_CODEC_DELAY = 6500000;
private static final long TEST_SEEK_PRE_ROLL = 80000000;
private static final int TEST_OPUS_CODEC_PRIVATE_SIZE = 2;
private static final String TEST_VORBIS_CODEC_PRIVATE = "webm/vorbis_codec_private";
private static final int TEST_VORBIS_INFO_SIZE = 30;
private static final int TEST_VORBIS_BOOKS_SIZE = 4140;
private static final byte[] TEST_ENCRYPTION_KEY_ID = { 0x00, 0x01, 0x02, 0x03 };
private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
private static final UUID ZERO_UUID = new UUID(0, 0);
// First 8 bytes of IV come from the container, last 8 bytes are always initialized to 0.
private static final byte[] TEST_INITIALIZATION_VECTOR = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
private static final int ID_VP9 = 0;
private static final int ID_OPUS = 1;
private static final int ID_VORBIS = 2;
private static final int EXPECTED_INIT_RESULT = WebmExtractor.RESULT_READ_INIT
| WebmExtractor.RESULT_READ_INDEX | WebmExtractor.RESULT_END_OF_STREAM;
private static final int EXPECTED_INIT_AND_SAMPLE_RESULT = WebmExtractor.RESULT_READ_INIT
| WebmExtractor.RESULT_READ_INDEX | WebmExtractor.RESULT_READ_SAMPLE;
private final WebmExtractor extractor = new WebmExtractor();
private final SampleHolder sampleHolder =
new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
@Override
public void setUp() {
sampleHolder.data = ByteBuffer.allocate(1024);
}
public void testPrepare() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareOpus() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_OPUS, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertAudioFormat(ID_OPUS);
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareVorbis() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VORBIS, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertAudioFormat(ID_VORBIS);
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
}
public void testPrepareContentEncodingEncryption() throws ParserException {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(new IndexPoint(0, 0, TEST_DURATION_US));
DrmInitData drmInitData = extractor.getDrmInitData();
assertNotNull(drmInitData);
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(WIDEVINE_UUID));
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(ZERO_UUID));
}
public void testPrepareThreeCuePoints() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(3, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(
new IndexPoint(0, 0, 10000),
new IndexPoint(10000, 0, 10000),
new IndexPoint(20000, 0, TEST_DURATION_US - 20000));
}
public void testPrepareCustomTimecodeScale() throws ParserException {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(3, 0, true, 1000, ID_VP9, null));
assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertIndex(
new IndexPoint(0, 0, 10),
new IndexPoint(10, 0, 10),
new IndexPoint(20, 0, (TEST_DURATION_US / 1000) - 20));
}
public void testPrepareNoCuePoints() {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(0, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("Invalid/missing cue points", exception.getMessage());
}
}
public void testPrepareInvalidDocType() {
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, false, DEFAULT_TIMECODE_SCALE, ID_VP9, null));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("DocType webB not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingOrder() {
ContentEncodingSettings settings = new ContentEncodingSettings(1, 1, 1, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingOrder 1 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingScope() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 0, 1, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingScope 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncodingType() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 0, 5, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncodingType 0 not supported", exception.getMessage());
}
}
public void testPrepareInvalidContentEncAlgo() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 4, 1);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("ContentEncAlgo 4 not supported", exception.getMessage());
}
}
public void testPrepareInvalidAESSettingsCipherMode() {
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 0);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(
createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings));
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("AESSettingsCipherMode 0 not supported", exception.getMessage());
}
}
public void testReadSampleKeyframe() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 0, true, false, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadBlock() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, false, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_OPUS, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertAudioFormat(ID_OPUS);
assertSample(mediaSegment, 0, true, false, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadEncryptedFrame() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, true);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 0, true, false, true);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadEncryptedFrameWithInvalidSignalByte() {
MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true, true, false);
ContentEncodingSettings settings = new ContentEncodingSettings(0, 1, 1, 5, 1);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, settings),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
try {
extractor.read(testInputStream, sampleHolder);
fail();
} catch (ParserException exception) {
assertEquals("Extension bit is set in signal byte", exception.getMessage());
}
}
public void testReadSampleInvisible() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, true, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 25000, false, true, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadSampleCustomTimescale() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, false, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, 1000, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 25, false, false, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
public void testReadSampleNegativeSimpleBlockTimecode() throws ParserException {
MediaSegment mediaSegment = createMediaSegment(100, 13, -12, true, true, true, false, false);
byte[] testInputData = joinByteArrays(
createInitializationSegment(
1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9, null),
mediaSegment.clusterBytes);
NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData);
assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder));
assertFormat();
assertSample(mediaSegment, 1000, true, true, false);
assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder));
}
private void assertFormat() {
MediaFormat format = extractor.getFormat();
assertEquals(TEST_WIDTH, format.width);
assertEquals(TEST_HEIGHT, format.height);
assertEquals(MimeTypes.VIDEO_VP9, format.mimeType);
}
private void assertAudioFormat(int codecId) {
MediaFormat format = extractor.getFormat();
assertEquals(TEST_CHANNEL_COUNT, format.channelCount);
assertEquals(TEST_SAMPLE_RATE, format.sampleRate);
if (codecId == ID_OPUS) {
assertEquals(MimeTypes.AUDIO_OPUS, format.mimeType);
assertEquals(3, format.initializationData.size());
assertEquals(TEST_OPUS_CODEC_PRIVATE_SIZE, format.initializationData.get(0).length);
assertEquals(TEST_CODEC_DELAY, ByteBuffer.wrap(format.initializationData.get(1)).getLong());
assertEquals(TEST_SEEK_PRE_ROLL, ByteBuffer.wrap(format.initializationData.get(2)).getLong());
} else if (codecId == ID_VORBIS) {
assertEquals(MimeTypes.AUDIO_VORBIS, format.mimeType);
assertEquals(2, format.initializationData.size());
assertEquals(TEST_VORBIS_INFO_SIZE, format.initializationData.get(0).length);
assertEquals(TEST_VORBIS_BOOKS_SIZE, format.initializationData.get(1).length);
}
}
private void assertIndex(IndexPoint... indexPoints) {
SegmentIndex index = extractor.getIndex();
assertEquals(CUES_ELEMENT_BYTE_SIZE + CUE_POINT_ELEMENT_BYTE_SIZE * indexPoints.length,
index.sizeBytes);
assertEquals(indexPoints.length, index.length);
for (int i = 0; i < indexPoints.length; i++) {
IndexPoint indexPoint = indexPoints[i];
assertEquals(indexPoint.timeUs, index.timesUs[i]);
assertEquals(indexPoint.size, index.sizes[i]);
assertEquals(indexPoint.durationUs, index.durationsUs[i]);
}
}
private void assertSample(
MediaSegment mediaSegment, int timeUs, boolean keyframe, boolean invisible,
boolean encrypted) {
assertTrue(Arrays.equals(
mediaSegment.videoBytes, Arrays.copyOf(sampleHolder.data.array(), sampleHolder.size)));
assertEquals(timeUs, sampleHolder.timeUs);
assertEquals(keyframe, sampleHolder.isSyncFrame());
assertEquals(invisible, sampleHolder.isDecodeOnly());
assertEquals(encrypted, sampleHolder.isEncrypted());
if (encrypted) {
android.test.MoreAsserts.assertEquals(TEST_INITIALIZATION_VECTOR, sampleHolder.cryptoInfo.iv);
assertEquals(C.CRYPTO_MODE_AES_CTR, sampleHolder.cryptoInfo.mode);
assertEquals(1, sampleHolder.cryptoInfo.numSubSamples);
assertEquals(100, sampleHolder.cryptoInfo.numBytesOfEncryptedData[0]);
assertEquals(0, sampleHolder.cryptoInfo.numBytesOfClearData[0]);
}
}
private byte[] createInitializationSegment(
int cuePoints, int mediaSegmentSize, boolean docTypeIsWebm, int timecodeScale,
int codecId, ContentEncodingSettings contentEncodingSettings) {
int initalizationSegmentSize = INFO_ELEMENT_BYTE_SIZE + TRACKS_ELEMENT_BYTE_SIZE
+ CUES_ELEMENT_BYTE_SIZE + CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints;
byte[] tracksElement = null;
switch (codecId) {
case ID_VP9:
tracksElement = createTracksElementWithVideo(
true, TEST_WIDTH, TEST_HEIGHT, contentEncodingSettings);
break;
case ID_OPUS:
tracksElement = createTracksElementWithOpusAudio(TEST_CHANNEL_COUNT);
break;
case ID_VORBIS:
tracksElement = createTracksElementWithVorbisAudio(TEST_CHANNEL_COUNT);
break;
}
byte[] bytes = joinByteArrays(createEbmlElement(1, docTypeIsWebm, 2),
createSegmentElement(initalizationSegmentSize + mediaSegmentSize),
createInfoElement(timecodeScale),
tracksElement,
createCuesElement(CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints));
for (int i = 0; i < cuePoints; i++) {
bytes = joinByteArrays(bytes, createCuePointElement(10 * i, initalizationSegmentSize));
}
return bytes;
}
private static MediaSegment createMediaSegment(int videoBytesLength, int clusterTimecode,
int blockTimecode, boolean keyframe, boolean invisible, boolean simple,
boolean encrypted, boolean validSignalByte) {
byte[] videoBytes = createVideoBytes(videoBytesLength);
byte[] blockBytes;
if (simple) {
blockBytes = createSimpleBlockElement(videoBytes.length, blockTimecode,
keyframe, invisible, true, encrypted, validSignalByte);
} else {
blockBytes = createBlockElement(videoBytes.length, blockTimecode, invisible, true);
}
byte[] clusterBytes =
createClusterElement(blockBytes.length + videoBytes.length, clusterTimecode);
return new MediaSegment(joinByteArrays(clusterBytes, blockBytes, videoBytes), videoBytes);
}
private static byte[] joinByteArrays(byte[]... byteArrays) {
int length = 0;
for (byte[] byteArray : byteArrays) {
length += byteArray.length;
}
byte[] joined = new byte[length];
length = 0;
for (byte[] byteArray : byteArrays) {
System.arraycopy(byteArray, 0, joined, length, byteArray.length);
length += byteArray.length;
}
return joined;
}
private static byte[] createEbmlElement(
int ebmlReadVersion, boolean docTypeIsWebm, int docTypeReadVersion) {
return createByteArray(
0x1A, 0x45, 0xDF, 0xA3, // EBML
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, // size=15
0x42, 0xF7, // EBMLReadVersion
0x81, ebmlReadVersion, // size=1
0x42, 0x82, // DocType
0x84, 0x77, 0x65, 0x62, docTypeIsWebm ? 0x6D : 0x42, // size=4 value=webm/B
0x42, 0x85, // DocTypeReadVersion
0x81, docTypeReadVersion); // size=1
}
private static byte[] createSegmentElement(int size) {
byte[] sizeBytes = getIntegerBytes(size);
return createByteArray(
0x18, 0x53, 0x80, 0x67, // Segment
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]);
}
private static byte[] createInfoElement(int timecodeScale) {
byte[] scaleBytes = getIntegerBytes(timecodeScale);
return createByteArray(
0x15, 0x49, 0xA9, 0x66, // Info
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, // size=19
0x2A, 0xD7, 0xB1, // TimecodeScale
0x84, scaleBytes[0], scaleBytes[1], scaleBytes[2], scaleBytes[3], // size=4
0x44, 0x89, // Duration
0x88, 0x40, 0xC3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00); // size=8 value=9920.0
}
private static byte[] createTracksElementWithVideo(
boolean codecIsVp9, int pixelWidth, int pixelHeight,
ContentEncodingSettings contentEncodingSettings) {
byte[] widthBytes = getIntegerBytes(pixelWidth);
byte[] heightBytes = getIntegerBytes(pixelHeight);
if (contentEncodingSettings != null) {
byte[] orderBytes = getIntegerBytes(contentEncodingSettings.order);
byte[] scopeBytes = getIntegerBytes(contentEncodingSettings.scope);
byte[] typeBytes = getIntegerBytes(contentEncodingSettings.type);
byte[] algorithmBytes = getIntegerBytes(contentEncodingSettings.algorithm);
byte[] cipherModeBytes = getIntegerBytes(contentEncodingSettings.aesCipherMode);
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, // size=72
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, // size=63
0x86, // CodecID
0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0
0x6D, 0x80, // ContentEncodings
0xA4, // size=36
0x62, 0x40, // ContentEncoding
0xA1, // size=33
0x50, 0x31, // ContentEncodingOrder
0x81, orderBytes[3],
0x50, 0x32, // ContentEncodingScope
0x81, scopeBytes[3],
0x50, 0x33, // ContentEncodingType
0x81, typeBytes[3],
0x50, 0x35, // ContentEncryption
0x92, // size=18
0x47, 0xE1, // ContentEncAlgo
0x81, algorithmBytes[3],
0x47, 0xE2, // ContentEncKeyID
0x84, // size=4
TEST_ENCRYPTION_KEY_ID[0], TEST_ENCRYPTION_KEY_ID[1],
TEST_ENCRYPTION_KEY_ID[2], TEST_ENCRYPTION_KEY_ID[3], // value=binary
0x47, 0xE7, // ContentEncAESSettings
0x84, // size=4
0x47, 0xE8, // AESSettingsCipherMode
0x81, cipherModeBytes[3],
0xE0, // Video
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8
0xB0, // PixelWidth
0x82, widthBytes[2], widthBytes[3], // size=2
0xBA, // PixelHeight
0x82, heightBytes[2], heightBytes[3]); // size=2
} else {
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, // size=36
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, // size=27
0x86, // CodecID
0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0
0xE0, // Video
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8
0xB0, // PixelWidth
0x82, widthBytes[2], widthBytes[3], // size=2
0xBA, // PixelHeight
0x82, heightBytes[2], heightBytes[3]); // size=2
}
}
private static byte[] createTracksElementWithOpusAudio(int channelCount) {
byte[] channelCountBytes = getIntegerBytes(channelCount);
return createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, // size=57
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, // size=48
0x86, // CodecID
0x86, 0x41, 0x5F, 0x4F, 0x50, 0x55, 0x53, // size=6 value=A_OPUS
0x56, 0xAA, // CodecDelay
0x83, 0x63, 0x2E, 0xA0, // size=3 value=6500000
0x56, 0xBB, // SeekPreRoll
0x84, 0x04, 0xC4, 0xB4, 0x00, // size=4 value=80000000
0xE1, // Audio
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13
0x9F, // Channels
0x81, channelCountBytes[3], // size=1
0xB5, // SamplingFrequency
0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000
0x63, 0xA2, // CodecPrivate
0x82, 0x00, 0x00); // size=2
}
private byte[] createTracksElementWithVorbisAudio(int channelCount) {
byte[] channelCountBytes = getIntegerBytes(channelCount);
byte[] tracksElement = createByteArray(
0x16, 0x54, 0xAE, 0x6B, // Tracks
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x9C, // size=4252
0xAE, // TrackEntry
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x93, // size=4243 (36+4207)
0x86, // CodecID
0x88, 0x41, 0x5f, 0x56, 0x4f, 0x52, 0x42, 0x49, 0x53, // size=8 value=A_VORBIS
0xE1, // Audio
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13
0x9F, // Channels
0x81, channelCountBytes[3], // size=1
0xB5, // SamplingFrequency
0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000
0x63, 0xA2, // CodecPrivate
0x50, 0x6F); // size=4207
byte[] codecPrivate = new byte[4207];
try {
getInstrumentation().getContext().getResources().getAssets().open(TEST_VORBIS_CODEC_PRIVATE)
.read(codecPrivate);
} catch (IOException e) {
fail(); // should never happen
}
return joinByteArrays(tracksElement, codecPrivate);
}
private static byte[] createCuesElement(int size) {
byte[] sizeBytes = getIntegerBytes(size);
return createByteArray(
0x1C, 0x53, 0xBB, 0x6B, // Cues
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); // size=31
}
private static byte[] createCuePointElement(int cueTime, int cueClusterPosition) {
byte[] positionBytes = getIntegerBytes(cueClusterPosition);
return createByteArray(
0xBB, // CuePoint
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, // size=22
0xB3, // CueTime
0x81, cueTime, // size=1
0xB7, // CueTrackPositions
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, // size=10
0xF1, // CueClusterPosition
0x88, 0x00, 0x00, 0x00, 0x00, positionBytes[0], positionBytes[1],
positionBytes[2], positionBytes[3]); // size=8
}
private static byte[] createClusterElement(int size, int timecode) {
byte[] sizeBytes = getIntegerBytes(size);
byte[] timeBytes = getIntegerBytes(timecode);
return createByteArray(
0x1F, 0x43, 0xB6, 0x75, // Cluster
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0xE7, // Timecode
0x84, timeBytes[0], timeBytes[1], timeBytes[2], timeBytes[3]); // size=4
}
private static byte[] createSimpleBlockElement(
int size, int timecode, boolean keyframe, boolean invisible, boolean noLacing,
boolean encrypted, boolean validSignalByte) {
byte[] sizeBytes = getIntegerBytes(size + 4 + (encrypted ? 9 : 0));
byte[] timeBytes = getIntegerBytes(timecode);
byte flags = (byte)
((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06));
byte[] simpleBlock = createByteArray(
0xA3, // SimpleBlock
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0x81, // Track number value=1
timeBytes[2], timeBytes[3], flags); // Timecode and flags
if (encrypted) {
simpleBlock = joinByteArrays(
simpleBlock, createByteArray(validSignalByte ? 0x01 : 0x80),
Arrays.copyOfRange(TEST_INITIALIZATION_VECTOR, 0, 8));
}
return simpleBlock;
}
private static byte[] createBlockElement(
int size, int timecode, boolean invisible, boolean noLacing) {
int blockSize = size + 4;
byte[] blockSizeBytes = getIntegerBytes(blockSize);
byte[] timeBytes = getIntegerBytes(timecode);
int blockElementSize = 1 + 8 + blockSize; // id + size + length of data
byte[] sizeBytes = getIntegerBytes(blockElementSize);
byte flags = (byte) ((invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06));
return createByteArray(
0xA0, // BlockGroup
0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3],
0xA1, // Block
0x01, 0x00, 0x00, 0x00,
blockSizeBytes[0], blockSizeBytes[1], blockSizeBytes[2], blockSizeBytes[3],
0x81, // Track number value=1
timeBytes[2], timeBytes[3], flags); // Timecode and flags
}
private static byte[] createVideoBytes(int size) {
byte[] videoBytes = new byte[size];
for (int i = 0; i < size; i++) {
videoBytes[i] = (byte) i;
}
return videoBytes;
}
private static byte[] getIntegerBytes(int value) {
return createByteArray(
(value & 0xFF000000) >> 24,
(value & 0x00FF0000) >> 16,
(value & 0x0000FF00) >> 8,
(value & 0x000000FF));
}
private static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
/** Used by {@link #createMediaSegment} to return both cluster and video bytes together. */
private static final class MediaSegment {
private final byte[] clusterBytes;
private final byte[] videoBytes;
private MediaSegment(byte[] clusterBytes, byte[] videoBytes) {
this.clusterBytes = clusterBytes;
this.videoBytes = videoBytes;
}
}
/** Used by {@link #assertIndex(IndexPoint...)} to validate index elements. */
private static final class IndexPoint {
private final long timeUs;
private final int size;
private final long durationUs;
private IndexPoint(long timeUs, int size, long durationUs) {
this.timeUs = timeUs;
this.size = size;
this.durationUs = durationUs;
}
}
/** Used by {@link #createTracksElementWithVideo} to create a Track header with Encryption. */
private static final class ContentEncodingSettings {
private final int order;
private final int scope;
private final int type;
private final int algorithm;
private final int aesCipherMode;
private ContentEncodingSettings(int order, int scope, int type, int algorithm,
int aesCipherMode) {
this.order = order;
this.scope = scope;
this.type = type;
this.algorithm = algorithm;
this.aesCipherMode = aesCipherMode;
}
}
}

View file

@ -15,35 +15,312 @@
*/ */
package com.google.android.exoplayer.dash; package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.MediaFormat; import static org.mockito.Mockito.when;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import junit.framework.TestCase; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.FixedEvaluator;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.dash.mpd.SegmentBase.MultiSegmentBase;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer.dash.mpd.UrlTemplate;
import com.google.android.exoplayer.testutil.Util;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.FakeClock;
import com.google.android.exoplayer.util.ManifestFetcher;
import android.test.InstrumentationTestCase;
import org.mockito.Mock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** /**
* Tests {@link DashChunkSource}. * Tests {@link DashChunkSource}.
*/ */
public class DashChunkSourceTest extends TestCase { public class DashChunkSourceTest extends InstrumentationTestCase {
private static final FormatEvaluator EVALUATOR = new FixedEvaluator();
private static final long AVAILABILITY_START_TIME = 0;
private static final long AVAILABILITY_LATENCY = 5000;
private static final long AVAILABILITY_REALTIME_OFFSET = 1000;
private static final long AVAILABILITY_CURRENT_TIME =
AVAILABILITY_START_TIME + AVAILABILITY_LATENCY - AVAILABILITY_REALTIME_OFFSET;
private static final FakeClock AVAILABILITY_CLOCK = new FakeClock(AVAILABILITY_CURRENT_TIME);
private static final int TALL_HEIGHT = 200;
private static final int WIDE_WIDTH = 400;
private static final Format REGULAR_VIDEO = new Format("1", "video/mp4", 480, 240, -1, -1, 1000);
private static final Format TALL_VIDEO = new Format("2", "video/mp4", 100, TALL_HEIGHT, -1, -1,
1000);
private static final Format WIDE_VIDEO = new Format("3", "video/mp4", WIDE_WIDTH, 50, -1, -1,
1000);
@Mock private DataSource mockDataSource;
@Mock private ManifestFetcher<MediaPresentationDescription> mockManifestFetcher;
@Override
public void setUp() throws Exception {
Util.setUpMockito(this);
}
public void testMaxVideoDimensions() { public void testMaxVideoDimensions() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO,
null, null, null);
MediaFormat out = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null);
chunkSource.getMaxVideoDimensions(out);
assertEquals(WIDE_WIDTH, out.getMaxVideoWidth());
assertEquals(TALL_HEIGHT, out.getMaxVideoHeight());
}
public void testMaxVideoDimensionsLegacy() {
SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4");
Format format1 = new Format("1", "video/mp4", 100, 200, -1, -1, 1000);
Representation representation1 = Representation representation1 =
Representation.newInstance(0, 0, null, 0, format1, segmentBase1); Representation.newInstance(0, 0, null, 0, TALL_VIDEO, segmentBase1);
SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4"); SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4");
Format format2 = new Format("2", "video/mp4", 400, 50, -1, -1, 1000);
Representation representation2 = Representation representation2 =
Representation.newInstance(0, 0, null, 0, format2, segmentBase2); Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2);
DashChunkSource chunkSource = new DashChunkSource(null, null, representation1, representation2); DashChunkSource chunkSource = new DashChunkSource(null, null, representation1, representation2);
MediaFormat out = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null); MediaFormat out = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null);
chunkSource.getMaxVideoDimensions(out); chunkSource.getMaxVideoDimensions(out);
assertEquals(400, out.getMaxVideoWidth()); assertEquals(WIDE_WIDTH, out.getMaxVideoWidth());
assertEquals(200, out.getMaxVideoHeight()); assertEquals(TALL_HEIGHT, out.getMaxVideoHeight());
}
public void testLiveEdgeNoLatencyWithTimeline() {
DashChunkSource chunkSource = setupLiveEdgeTimelineTest(0L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge500msLatencyWithTimeline() {
DashChunkSource chunkSource = setupLiveEdgeTimelineTest(500L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge1000msLatencyWithTimeline() {
DashChunkSource chunkSource = setupLiveEdgeTimelineTest(1000L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge1001msLatencyWithTimeline() {
DashChunkSource chunkSource = setupLiveEdgeTimelineTest(1001L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(3000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(4000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge2500msLatencyWithTimeline() {
DashChunkSource chunkSource = setupLiveEdgeTimelineTest(2500L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(2000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(3000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdgeVeryHighLatencyWithTimeline() {
DashChunkSource chunkSource = setupLiveEdgeTimelineTest(10000L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(0L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(1000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdgeNoLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(0L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
// this should actually return the "5th" segment, but it currently returns the "6th", which
// doesn't actually exist yet; this will be resolved in a subsequent cl (cl/87518875).
//assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
//assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdgeAlmostNoLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge500msLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(500L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge1000msLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1000L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge1001msLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1001L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(3000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(4000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdge2500msLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(2500L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(2000000L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(3000000L, ((MediaChunk) out.chunk).endTimeUs);
}
public void testLiveEdgeVeryHighLatencyWithTemplate() {
DashChunkSource chunkSource = setupLiveEdgeTemplateTest(10000L);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
chunkSource.getChunkOperation(queue, 0, 0, out);
assertEquals(0L, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(1000000L, ((MediaChunk) out.chunk).endTimeUs);
}
private static MediaPresentationDescription generateMpd(boolean live,
List<Representation> representations) {
Representation firstRepresentation = representations.get(0);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
Period period = new Period(null, firstRepresentation.periodStartMs,
firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet));
long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US
: firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
return new MediaPresentationDescription(AVAILABILITY_START_TIME, duration, -1, live, -1, -1,
null, Collections.singletonList(period));
}
private static MediaPresentationDescription generateVodMpd() {
List<Representation> representations = new ArrayList<Representation>();
SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4");
Representation representation1 =
Representation.newInstance(0, 0, null, 0, TALL_VIDEO, segmentBase1);
representations.add(representation1);
SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4");
Representation representation2 =
Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2);
representations.add(representation2);
return generateMpd(false, representations);
}
private static MediaPresentationDescription generateLiveMpdWithTimeline() {
List<Representation> representations = new ArrayList<Representation>();
List<SegmentTimelineElement> segmentTimeline = new ArrayList<SegmentTimelineElement>();
segmentTimeline.add(new SegmentTimelineElement(0L, 1000L));
segmentTimeline.add(new SegmentTimelineElement(1000L, 1000L));
segmentTimeline.add(new SegmentTimelineElement(2000L, 1000L));
segmentTimeline.add(new SegmentTimelineElement(3000L, 1000L));
segmentTimeline.add(new SegmentTimelineElement(4000L, 1000L));
List<RangedUri> mediaSegments = new ArrayList<RangedUri>();
mediaSegments.add(new RangedUri("", "", 0L, 500L));
mediaSegments.add(new RangedUri("", "", 500L, 500L));
mediaSegments.add(new RangedUri("", "", 1000L, 500L));
mediaSegments.add(new RangedUri("", "", 1500L, 500L));
mediaSegments.add(new RangedUri("", "", 2000L, 500L));
MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0,
TrackRenderer.UNKNOWN_TIME_US, 1, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline,
mediaSegments);
Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US,
null, 0, REGULAR_VIDEO, segmentBase);
representations.add(representation);
return generateMpd(true, representations);
}
private static MediaPresentationDescription generateLiveMpdWithTemplate() {
List<Representation> representations = new ArrayList<Representation>();
UrlTemplate initializationTemplate = null;
UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$");
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0,
TrackRenderer.UNKNOWN_TIME_US, 1, 1000, null,
initializationTemplate, mediaTemplate, "http://www.youtube.com");
Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US,
null, 0, REGULAR_VIDEO, segmentBase);
representations.add(representation);
return generateMpd(true, representations);
}
private DashChunkSource setupLiveEdgeTimelineTest(long liveEdgeLatencyMs) {
MediaPresentationDescription manifest = generateLiveMpdWithTimeline();
when(mockManifestFetcher.getManifest()).thenReturn(manifest);
return new DashChunkSource(mockManifestFetcher, manifest, AdaptationSet.TYPE_VIDEO, null,
mockDataSource, EVALUATOR, AVAILABILITY_CLOCK, liveEdgeLatencyMs * 1000,
AVAILABILITY_REALTIME_OFFSET * 1000);
}
private DashChunkSource setupLiveEdgeTemplateTest(long liveEdgeLatencyMs) {
MediaPresentationDescription manifest = generateLiveMpdWithTemplate();
when(mockManifestFetcher.getManifest()).thenReturn(manifest);
return new DashChunkSource(mockManifestFetcher, manifest, AdaptationSet.TYPE_VIDEO, null,
mockDataSource, EVALUATOR, AVAILABILITY_CLOCK, liveEdgeLatencyMs * 1000,
AVAILABILITY_REALTIME_OFFSET * 1000);
} }
} }

View file

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

View file

@ -1,149 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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);
}
}

View file

@ -1,60 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.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));
}
}

View file

@ -0,0 +1,34 @@
/*
* 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.util;
/**
* A {@link Clock} that returns a fixed value specified in the constructor.
*/
public class FakeClock implements Clock {
private final long timeMs;
public FakeClock(long timeMs) {
this.timeMs = timeMs;
}
@Override
public long elapsedRealtime() {
return timeMs;
}
}