diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml
index 98ed2f18a9..8812dbc014 100644
--- a/demo/src/main/AndroidManifest.xml
+++ b/demo/src/main/AndroidManifest.xml
@@ -16,8 +16,8 @@
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
index c2dc0ff1c7..39b21b62bf 100644
--- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java
@@ -134,6 +134,8 @@ package com.google.android.exoplayer.demo;
public static final Sample[] MISC = new Sample[] {
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
DemoUtil.TYPE_OTHER, false, true),
+ new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj",
+ DemoUtil.TYPE_OTHER, false, true),
};
private Samples() {}
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java
index 221d2c6daa..4ee13a75fc 100644
--- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java
@@ -28,8 +28,7 @@ import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
-import com.google.android.exoplayer.dash.DashMp4ChunkSource;
-import com.google.android.exoplayer.dash.DashWebmChunkSource;
+import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
@@ -163,14 +162,8 @@ public class DashVodRendererBuilder implements RendererBuilder,
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource;
String mimeType = videoRepresentations[0].format.mimeType;
- if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
- videoChunkSource = new DashMp4ChunkSource(videoDataSource,
- new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
- } else if (mimeType.equals(MimeTypes.VIDEO_WEBM)) {
- // TODO: Figure out how to query supported vpX resolutions. For now, restrict to standard
- // definition streams.
- videoRepresentations = getSdRepresentations(videoRepresentations);
- videoChunkSource = new DashWebmChunkSource(videoDataSource,
+ if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) {
+ videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
} else {
throw new IllegalStateException("Unexpected mime type: " + mimeType);
@@ -200,7 +193,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
Format format = representation.format;
audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " +
format.audioSamplingRate + "Hz)";
- audioChunkSources[i] = new DashMp4ChunkSource(audioDataSource,
+ audioChunkSources[i] = new DashChunkSource(audioDataSource,
audioEvaluator, representation);
}
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java
index e3ee3d46b3..a253de873e 100644
--- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java
+++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java
@@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
-import com.google.android.exoplayer.dash.DashMp4ChunkSource;
+import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
@@ -116,7 +116,7 @@ import java.util.ArrayList;
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
- ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
+ ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
@@ -125,7 +125,7 @@ import java.util.ArrayList;
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
- ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
+ ChunkSource audioChunkSource = new DashChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
index 99c378adad..3621489f3b 100644
--- a/library/src/main/AndroidManifest.xml
+++ b/library/src/main/AndroidManifest.xml
@@ -21,6 +21,12 @@
+
+
diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
index 06973db73b..cc3265d18a 100644
--- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
+++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
@@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
- public static final String VERSION = "1.0.12";
+ public static final String VERSION = "1.0.13";
/**
* The version of the library, expressed as an integer.
@@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo {
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
* corresponding integer version 1002003.
*/
- public static final int VERSION_INT = 1000012;
+ public static final int VERSION_INT = 1000013;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java
index 908664495f..63afbdf0f5 100644
--- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java
@@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource {
}
@Override
- public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
+ public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java
index 798282dd91..dbe7ac47cc 100644
--- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java
@@ -27,6 +27,7 @@ import android.media.AudioTimestamp;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaFormat;
+import android.media.audiofx.Virtualizer;
import android.os.ConditionVariable;
import android.os.Handler;
import android.util.Log;
@@ -91,6 +92,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private static final long MICROS_PER_SECOND = 1000000L;
+ /**
+ * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
+ * than this amount. This is a fail safe that should not be required on correctly functioning
+ * devices.
+ */
+ private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND;
+
private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
@@ -358,9 +366,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
* subsequently re-enabled.
*
* The default implementation is a no-op. One reason for overriding this method would be to
- * instantiate and enable a {@link android.media.audiofx.Virtualizer} in order to spatialize the
- * audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances
- * should be released in {@link #onDisabled()} (if not before).
+ * instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
+ * this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
+ * (if not before).
*
* @param audioSessionId The audio session id.
*/
@@ -425,7 +433,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override
protected boolean isReady() {
- return super.isReady() || getPendingFrameCount() > 0;
+ return getPendingFrameCount() > 0
+ || (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL);
}
/**
@@ -500,11 +509,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
- if (audioTimestampSet
- && (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) {
- // The timestamp was set, but it corresponds to a time before the track was most recently
- // resumed.
- audioTimestampSet = false;
+ if (audioTimestampSet) {
+ // Perform sanity checks on the timestamp.
+ long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000;
+ if (audioTimestampUs < audioTrackResumeSystemTimeUs) {
+ // The timestamp corresponds to a time before the track was most recently resumed.
+ audioTimestampSet = false;
+ } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) {
+ // The timestamp time base is probably wrong.
+ audioTimestampSet = false;
+ Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", "
+ + audioTimestampUs + ", " + systemClockUs);
+ }
}
if (audioTrackGetLatencyMethod != null) {
try {
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
index 6f7262f79a..a23f5edf49 100644
--- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
@@ -78,6 +78,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
+ /**
+ * Value of {@link #sourceState} when the source is not ready.
+ */
+ protected static final int SOURCE_STATE_NOT_READY = 0;
+ /**
+ * Value of {@link #sourceState} when the source is ready and we're able to read from it.
+ */
+ protected static final int SOURCE_STATE_READY = 1;
+ /**
+ * Value of {@link #sourceState} when the source is ready but we might not be able to read from
+ * it. We transition to this state when an attempt to read a sample fails despite the source
+ * reporting that samples are available. This can occur when the next sample to be provided by
+ * the source is for another renderer.
+ */
+ protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
+
/**
* If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
* time during which {@link #isReady()} will report true regardless of whether the new codec has
@@ -108,7 +124,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private final boolean playClearSamplesWithoutKeys;
private final SampleSource source;
private final SampleHolder sampleHolder;
- private final FormatHolder formatHolder;
+ private final MediaFormatHolder formatHolder;
private final HashSet decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo;
private final EventListener eventListener;
@@ -128,7 +144,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private int codecReconfigurationState;
private int trackIndex;
- private boolean sourceIsReady;
+ private int sourceState;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeys;
@@ -158,7 +174,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
this.eventListener = eventListener;
codecCounters = new CodecCounters();
sampleHolder = new SampleHolder(false);
- formatHolder = new FormatHolder();
+ formatHolder = new MediaFormatHolder();
decodeOnlyPresentationTimestamps = new HashSet();
outputBufferInfo = new MediaCodec.BufferInfo();
}
@@ -202,7 +218,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override
protected void onEnabled(long timeUs, boolean joining) {
source.enable(trackIndex, timeUs);
- sourceIsReady = false;
+ sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false;
outputStreamEnded = false;
waitingForKeys = false;
@@ -267,9 +283,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
SystemClock.elapsedRealtime() : -1;
- inputIndex = -1;
- outputIndex = -1;
- waitingForFirstSyncFrame = true;
+ inputIndex = -1;
+ outputIndex = -1;
+ waitingForFirstSyncFrame = true;
codecCounters.codecInitCount++;
}
@@ -353,7 +369,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected void seekTo(long timeUs) throws ExoPlaybackException {
currentPositionUs = timeUs;
source.seekToUs(timeUs);
- sourceIsReady = false;
+ sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false;
outputStreamEnded = false;
waitingForKeys = false;
@@ -372,7 +388,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
try {
- sourceIsReady = source.continueBuffering(timeUs);
+ sourceState = source.continueBuffering(timeUs)
+ ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
+ : SOURCE_STATE_NOT_READY;
checkForDiscontinuity();
if (format == null) {
readFormat();
@@ -384,7 +402,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
if (codec != null) {
while (drainOutputBuffer(timeUs)) {}
- while (feedInputBuffer()) {}
+ if (feedInputBuffer(true)) {
+ while (feedInputBuffer(false)) {}
+ }
}
}
codecCounters.ensureUpdated();
@@ -429,6 +449,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecHotswapTimeMs = -1;
inputIndex = -1;
outputIndex = -1;
+ waitingForFirstSyncFrame = true;
decodeOnlyPresentationTimestamps.clear();
// Workaround for framework bugs.
// See [redacted], [redacted], [redacted].
@@ -446,11 +467,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
/**
+ * @param firstFeed True if this is the first call to this method from the current invocation of
+ * {@link #doSomeWork(long)}. False otherwise.
* @return True if it may be possible to feed more input data. False otherwise.
* @throws IOException If an error occurs reading data from the upstream source.
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
*/
- private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
+ private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException {
if (inputStreamEnded) {
return false;
}
@@ -478,6 +501,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
}
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
+ if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) {
+ sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
+ }
}
if (result == SampleSource.NOTHING_READ) {
@@ -594,7 +620,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @param formatHolder Holds the new format.
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
*/
- private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
+ private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
MediaFormat oldFormat = format;
format = formatHolder.format;
drmInitData = formatHolder.drmInitData;
@@ -646,7 +672,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override
protected boolean isReady() {
return format != null && !waitingForKeys
- && (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod());
+ && sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod();
+ }
+
+ /**
+ * Gets the source state.
+ *
+ * @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and
+ * {@link #SOURCE_STATE_READY_READ_MAY_FAIL}.
+ */
+ protected final int getSourceState() {
+ return sourceState;
}
private boolean isWithinHotswapPeriod() {
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
index b941767955..0fd1f4fd47 100644
--- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
@@ -235,7 +235,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override
protected boolean isReady() {
- if (super.isReady()) {
+ if (super.isReady() && (renderedFirstFrame || !codecInitialized()
+ || getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineUs = -1;
return true;
diff --git a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java
index 3188e36db0..d703a72a84 100644
--- a/library/src/main/java/com/google/android/exoplayer/MediaFormat.java
+++ b/library/src/main/java/com/google/android/exoplayer/MediaFormat.java
@@ -27,7 +27,7 @@ import java.util.Collections;
import java.util.List;
/**
- * Encapsulates the information describing the format of media data, be it audio or video.
+ * Defines the format of an elementary media stream.
*/
public class MediaFormat {
diff --git a/library/src/main/java/com/google/android/exoplayer/FormatHolder.java b/library/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java
similarity index 96%
rename from library/src/main/java/com/google/android/exoplayer/FormatHolder.java
rename to library/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java
index 04a6d7d85f..621a0f7986 100644
--- a/library/src/main/java/com/google/android/exoplayer/FormatHolder.java
+++ b/library/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java
@@ -21,7 +21,7 @@ import java.util.UUID;
/**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
*/
-public final class FormatHolder {
+public final class MediaFormatHolder {
/**
* The format of the media.
diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java
index fc29ef1ad5..2f26d30e9a 100644
--- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java
@@ -80,7 +80,7 @@ public interface SampleSource {
/**
* Enable the specified track. This allows the track's format and samples to be read from
- * {@link #readData(int, long, FormatHolder, SampleHolder, boolean)}.
+ * {@link #readData(int, long, MediaFormatHolder, SampleHolder, boolean)}.
*
* This method should not be called until after the source has been successfully prepared.
*
@@ -119,7 +119,7 @@ public interface SampleSource {
*
* @param track The track from which to read.
* @param playbackPositionUs The current playback position.
- * @param formatHolder A {@link FormatHolder} object to populate in the case of a new format.
+ * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
* references a valid output buffer.
@@ -129,7 +129,7 @@ public interface SampleSource {
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
* @throws IOException If an error occurred reading from the source.
*/
- public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
+ public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
/**
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
index 980de4ec23..c59b321c9c 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
@@ -16,9 +16,9 @@
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
-import com.google.android.exoplayer.FormatHolder;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
@@ -267,7 +267,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
}
@Override
- public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
+ public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
Assertions.checkState(state == STATE_ENABLED);
Assertions.checkState(track == 0);
@@ -318,6 +318,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
}
if (!mediaChunk.prepare()) {
+ if (currentLoadableException != null) {
+ throw currentLoadableException;
+ }
return NOTHING_READ;
}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
index 3482d160fc..b2948c06a9 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java
@@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions;
import java.util.Comparator;
/**
- * A format definition for streams.
+ * Defines the high level format of a media stream.
*/
public class Format {
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
index 01033e73f6..2793496b53 100644
--- a/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
+++ b/library/src/main/java/com/google/android/exoplayer/chunk/Mp4MediaChunk.java
@@ -18,7 +18,7 @@ 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.parser.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
@@ -32,7 +32,7 @@ import java.util.UUID;
*/
public final class Mp4MediaChunk extends MediaChunk {
- private final FragmentedMp4Extractor extractor;
+ private final Extractor extractor;
private final boolean maybeSelfContained;
private final long sampleOffsetUs;
@@ -57,7 +57,7 @@ public final class Mp4MediaChunk extends MediaChunk {
*/
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
- FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
+ Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
@@ -89,7 +89,7 @@ public final class Mp4MediaChunk extends MediaChunk {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, null);
- prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
+ 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.
@@ -107,7 +107,7 @@ public final class Mp4MediaChunk extends MediaChunk {
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
- return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
+ return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override
@@ -115,7 +115,7 @@ public final class Mp4MediaChunk extends MediaChunk {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder);
- boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
+ boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0;
if (sampleRead) {
holder.timeUs -= sampleOffsetUs;
}
diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
deleted file mode 100644
index 4769da9772..0000000000
--- a/library/src/main/java/com/google/android/exoplayer/chunk/WebmMediaChunk.java
+++ /dev/null
@@ -1,99 +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;
-
-import com.google.android.exoplayer.MediaFormat;
-import com.google.android.exoplayer.ParserException;
-import com.google.android.exoplayer.SampleHolder;
-import com.google.android.exoplayer.parser.webm.WebmExtractor;
-import com.google.android.exoplayer.upstream.DataSource;
-import com.google.android.exoplayer.upstream.DataSpec;
-import com.google.android.exoplayer.upstream.NonBlockingInputStream;
-import com.google.android.exoplayer.util.Assertions;
-
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * A WebM {@link MediaChunk}.
- */
-public final class WebmMediaChunk extends MediaChunk {
-
- private final WebmExtractor extractor;
-
- /**
- * @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 extractor The extractor that will be used to extract the samples.
- * @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.
- */
- public WebmMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
- int trigger, WebmExtractor extractor, long startTimeUs, long endTimeUs,
- int nextChunkIndex) {
- super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
- this.extractor = extractor;
- }
-
- @Override
- public void seekToStart() {
- seekTo(0, false);
- }
-
- @Override
- public boolean seekTo(long positionUs, boolean allowNoop) {
- boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
- if (isDiscontinuous) {
- resetReadPosition();
- }
- return isDiscontinuous;
- }
-
- @Override
- public boolean prepare() {
- return true;
- }
-
- @Override
- public boolean sampleAvailable() throws ParserException {
- NonBlockingInputStream inputStream = getNonBlockingInputStream();
- int result = extractor.read(inputStream, null);
- return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
- }
-
- @Override
- public boolean read(SampleHolder holder) {
- NonBlockingInputStream inputStream = getNonBlockingInputStream();
- Assertions.checkState(inputStream != null);
- int result = extractor.read(inputStream, holder);
- return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
- }
-
- @Override
- public MediaFormat getMediaFormat() {
- return extractor.getFormat();
- }
-
- @Override
- public Map getPsshInfo() {
- // TODO: Add support for Pssh to WebmExtractor
- return null;
- }
-
-}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
similarity index 83%
rename from library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
rename to library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
index 0013df15a4..997da914cb 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
@@ -29,10 +29,13 @@ import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
+import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
@@ -42,9 +45,11 @@ import java.util.HashMap;
import java.util.List;
/**
- * An {@link ChunkSource} for Mp4 DASH streams.
+ * An {@link ChunkSource} for DASH streams.
+ *
+ * This implementation currently supports fMP4 and webm.
*/
-public class DashMp4ChunkSource implements ChunkSource {
+public class DashChunkSource implements ChunkSource {
private final TrackInfo trackInfo;
private final DataSource dataSource;
@@ -55,7 +60,7 @@ public class DashMp4ChunkSource implements ChunkSource {
private final Format[] formats;
private final HashMap representations;
- private final HashMap extractors;
+ private final HashMap extractors;
private final HashMap segmentIndexes;
private boolean lastChunkWasInitialization;
@@ -65,12 +70,12 @@ public class DashMp4ChunkSource implements ChunkSource {
* @param evaluator Selects from the available formats.
* @param representations The representations to be considered by the source.
*/
- public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
+ public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator,
Representation... representations) {
this.dataSource = dataSource;
this.evaluator = evaluator;
this.formats = new Format[representations.length];
- this.extractors = new HashMap();
+ this.extractors = new HashMap();
this.segmentIndexes = new HashMap();
this.representations = new HashMap();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
@@ -82,7 +87,9 @@ public class DashMp4ChunkSource implements ChunkSource {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
- extractors.put(formats[i].id, new FragmentedMp4Extractor());
+ Extractor extractor = formats[i].mimeType.startsWith(MimeTypes.VIDEO_WEBM)
+ ? new WebmExtractor() : new FragmentedMp4Extractor();
+ extractors.put(formats[i].id, extractor);
this.representations.put(formats[i].id, representations[i]);
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
@@ -142,7 +149,7 @@ public class DashMp4ChunkSource implements ChunkSource {
}
Representation selectedRepresentation = representations.get(selectedFormat.id);
- FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
+ Extractor extractor = extractors.get(selectedRepresentation.format.id);
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
@@ -191,35 +198,39 @@ public class DashMp4ChunkSource implements ChunkSource {
}
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
- Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
+ Representation representation, Extractor extractor, DataSource dataSource,
int trigger) {
- int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM;
+ int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM;
long indexAnchor = 0;
RangedUri requestUri;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
- expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
+ expectedExtractorResult |= Extractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) {
- expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
- indexAnchor = indexUri.start + indexUri.length;
+ expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
+ if (extractor.hasRelativeIndexOffsets()) {
+ indexAnchor = indexUri.start + indexUri.length;
+ }
} else {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
- indexAnchor = indexUri.start + indexUri.length;
- expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
+ if (extractor.hasRelativeIndexOffsets()) {
+ indexAnchor = indexUri.start + indexUri.length;
+ }
+ expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey());
- return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
+ return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor);
}
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
- FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
+ Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
@@ -232,15 +243,15 @@ public class DashMp4ChunkSource implements ChunkSource {
endTimeUs, nextSegmentNum, extractor, false, 0);
}
- private class InitializationMp4Loadable extends Chunk {
+ private class InitializationLoadable extends Chunk {
- private final FragmentedMp4Extractor extractor;
+ private final Extractor extractor;
private final int expectedExtractorResult;
private final long indexAnchor;
private final Uri uri;
- public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
- Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
+ public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
+ Format format, Extractor extractor, int expectedExtractorResult,
long indexAnchor) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor;
@@ -256,7 +267,7 @@ public class DashMp4ChunkSource implements ChunkSource {
throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
}
- if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
+ if ((result & Extractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java
deleted file mode 100644
index 2f01a38120..0000000000
--- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java
+++ /dev/null
@@ -1,261 +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.dash;
-
-import com.google.android.exoplayer.MediaFormat;
-import com.google.android.exoplayer.ParserException;
-import com.google.android.exoplayer.TrackInfo;
-import com.google.android.exoplayer.chunk.Chunk;
-import com.google.android.exoplayer.chunk.ChunkOperationHolder;
-import com.google.android.exoplayer.chunk.ChunkSource;
-import com.google.android.exoplayer.chunk.Format;
-import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
-import com.google.android.exoplayer.chunk.FormatEvaluator;
-import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
-import com.google.android.exoplayer.chunk.MediaChunk;
-import com.google.android.exoplayer.chunk.WebmMediaChunk;
-import com.google.android.exoplayer.dash.mpd.RangedUri;
-import com.google.android.exoplayer.dash.mpd.Representation;
-import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
-import com.google.android.exoplayer.parser.webm.WebmExtractor;
-import com.google.android.exoplayer.upstream.DataSource;
-import com.google.android.exoplayer.upstream.DataSpec;
-import com.google.android.exoplayer.upstream.NonBlockingInputStream;
-
-import android.net.Uri;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * An {@link ChunkSource} for WebM DASH streams.
- */
-public class DashWebmChunkSource implements ChunkSource {
-
- private final TrackInfo trackInfo;
- private final DataSource dataSource;
- private final FormatEvaluator evaluator;
- private final Evaluation evaluation;
- private final int maxWidth;
- private final int maxHeight;
-
- private final Format[] formats;
- private final HashMap representations;
- private final HashMap extractors;
- private final HashMap segmentIndexes;
-
- private boolean lastChunkWasInitialization;
-
- /**
- * @param dataSource A {@link DataSource} suitable for loading the media data.
- * @param evaluator Selects from the available formats.
- * @param representations The representations to be considered by the source.
- */
- public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
- Representation... representations) {
- this.dataSource = dataSource;
- this.evaluator = evaluator;
- this.formats = new Format[representations.length];
- this.extractors = new HashMap();
- this.segmentIndexes = new HashMap();
- this.representations = new HashMap();
- this.trackInfo = new TrackInfo(representations[0].format.mimeType,
- representations[0].periodDurationMs * 1000);
- this.evaluation = new Evaluation();
- int maxWidth = 0;
- int maxHeight = 0;
- for (int i = 0; i < representations.length; i++) {
- formats[i] = representations[i].format;
- maxWidth = Math.max(formats[i].width, maxWidth);
- maxHeight = Math.max(formats[i].height, maxHeight);
- extractors.put(formats[i].id, new DefaultWebmExtractor());
- this.representations.put(formats[i].id, representations[i]);
- DashSegmentIndex segmentIndex = representations[i].getIndex();
- if (segmentIndex != null) {
- segmentIndexes.put(formats[i].id, segmentIndex);
- }
- }
- this.maxWidth = maxWidth;
- this.maxHeight = maxHeight;
- Arrays.sort(formats, new DecreasingBandwidthComparator());
- }
-
- @Override
- public final void getMaxVideoDimensions(MediaFormat out) {
- if (trackInfo.mimeType.startsWith("video")) {
- out.setMaxVideoDimensions(maxWidth, maxHeight);
- }
- }
-
- @Override
- public final TrackInfo getTrackInfo() {
- return trackInfo;
- }
-
- @Override
- public void enable() {
- evaluator.enable();
- }
-
- @Override
- public void disable(List extends MediaChunk> queue) {
- evaluator.disable();
- }
-
- @Override
- public void continueBuffering(long playbackPositionUs) {
- // Do nothing
- }
-
- @Override
- public final void getChunkOperation(List extends MediaChunk> queue, long seekPositionUs,
- long playbackPositionUs, ChunkOperationHolder out) {
- evaluation.queueSize = queue.size();
- if (evaluation.format == null || !lastChunkWasInitialization) {
- evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
- }
- Format selectedFormat = evaluation.format;
- out.queueSize = evaluation.queueSize;
-
- if (selectedFormat == null) {
- out.chunk = null;
- return;
- } else if (out.queueSize == queue.size() && out.chunk != null
- && out.chunk.format.id.equals(selectedFormat.id)) {
- // We already have a chunk, and the evaluation hasn't changed either the format or the size
- // of the queue. Leave unchanged.
- return;
- }
-
- Representation selectedRepresentation = representations.get(selectedFormat.id);
- WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
-
- RangedUri pendingInitializationUri = null;
- RangedUri pendingIndexUri = null;
- if (extractor.getFormat() == null) {
- pendingInitializationUri = selectedRepresentation.getInitializationUri();
- }
- if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
- pendingIndexUri = selectedRepresentation.getIndexUri();
- }
- if (pendingInitializationUri != null || pendingIndexUri != null) {
- // We have initialization and/or index requests to make.
- Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
- selectedRepresentation, extractor, dataSource, evaluation.trigger);
- lastChunkWasInitialization = true;
- out.chunk = initializationChunk;
- return;
- }
-
- int nextSegmentNum;
- DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
- if (queue.isEmpty()) {
- nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
- } else {
- nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
- }
-
- if (nextSegmentNum == -1) {
- out.chunk = null;
- return;
- }
-
- Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
- dataSource, nextSegmentNum, evaluation.trigger);
- lastChunkWasInitialization = false;
- out.chunk = nextMediaChunk;
- }
-
- @Override
- public IOException getError() {
- return null;
- }
-
- @Override
- public void onChunkLoadError(Chunk chunk, Exception e) {
- // Do nothing.
- }
-
- private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
- Representation representation, WebmExtractor extractor, DataSource dataSource,
- int trigger) {
- int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM;
- RangedUri requestUri;
- if (initializationUri != null) {
- // It's common for initialization and index data to be stored adjacently. Attempt to merge
- // the two requests together to request both at once.
- expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT;
- requestUri = initializationUri.attemptMerge(indexUri);
- if (requestUri != null) {
- expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
- } else {
- requestUri = initializationUri;
- }
- } else {
- requestUri = indexUri;
- expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
- }
- DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
- representation.getCacheKey());
- return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
- extractor, expectedExtractorResult);
- }
-
- private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
- WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
- int lastSegmentNum = segmentIndex.getLastSegmentNum();
- int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
- long startTimeUs = segmentIndex.getTimeUs(segmentNum);
- long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
- : startTimeUs + segmentIndex.getDurationUs(segmentNum);
- RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
- DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
- representation.getCacheKey());
- return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
- startTimeUs, endTimeUs, nextSegmentNum);
- }
-
- private class InitializationWebmLoadable extends Chunk {
-
- private final WebmExtractor extractor;
- private final int expectedExtractorResult;
- private final Uri uri;
-
- public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
- Format format, WebmExtractor extractor, int expectedExtractorResult) {
- super(dataSource, dataSpec, format, trigger);
- this.extractor = extractor;
- this.expectedExtractorResult = expectedExtractorResult;
- 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 & WebmExtractor.RESULT_READ_INDEX) != 0) {
- segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0));
- }
- }
-
- }
-
-}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java
index 6d7b35a450..98e85ac40b 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java
@@ -23,6 +23,8 @@ import java.util.List;
*/
public final class MediaPresentationDescription {
+ public final long availabilityStartTime;
+
public final long duration;
public final long minBufferTime;
@@ -31,14 +33,22 @@ public final class MediaPresentationDescription {
public final long minUpdatePeriod;
+ public final long timeShiftBufferDepth;
+
public final List periods;
- public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic,
- long minUpdatePeriod, List periods) {
+ public final UtcTimingElement utcTiming;
+
+ public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
+ boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
+ List periods) {
+ this.availabilityStartTime = availabilityStartTime;
this.duration = duration;
this.minBufferTime = minBufferTime;
this.dynamic = dynamic;
this.minUpdatePeriod = minUpdatePeriod;
+ this.timeShiftBufferDepth = timeShiftBufferDepth;
+ this.utcTiming = utcTiming;
this.periods = Collections.unmodifiableList(periods);
}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java
index da294190f7..45885cfc90 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java
@@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
-import org.xmlpull.v1.XmlPullParserException;
-
import java.io.IOException;
import java.io.InputStream;
@@ -60,11 +58,7 @@ public final class MediaPresentationDescriptionFetcher extends
@Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId, Uri baseUrl) throws IOException, ParserException {
- try {
- return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
- } catch (XmlPullParserException e) {
- throw new ParserException(e);
- }
+ return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
index 2bd53d998b..0011c5d225 100644
--- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
@@ -34,8 +34,13 @@ import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
+import java.math.BigDecimal;
+import java.text.ParseException;
import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
import java.util.List;
+import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -48,6 +53,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
private static final Pattern DURATION =
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
+ private static final Pattern DATE_TIME_PATTERN =
+ Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?"
+ + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
+
private final XmlPullParserFactory xmlParserFactory;
public MediaPresentationDescriptionParser() {
@@ -69,42 +79,57 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
* @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream.
- * @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
*/
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
- String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
- IOException, ParserException {
- XmlPullParser xpp = xmlParserFactory.newPullParser();
- xpp.setInput(inputStream, inputEncoding);
- int eventType = xpp.next();
- if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
- throw new ParserException(
- "inputStream does not contain a valid media presentation description");
+ String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException {
+ try {
+ XmlPullParser xpp = xmlParserFactory.newPullParser();
+ xpp.setInput(inputStream, inputEncoding);
+ int eventType = xpp.next();
+ if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
+ throw new ParserException(
+ "inputStream does not contain a valid media presentation description");
+ }
+ return parseMediaPresentationDescription(xpp, contentId, baseUrl);
+ } catch (XmlPullParserException e) {
+ throw new ParserException(e);
+ } catch (ParseException e) {
+ throw new ParserException(e);
}
- return parseMediaPresentationDescription(xpp, contentId, baseUrl);
}
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
- String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
+ String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
+ long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
+ long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1;
+ UtcTimingElement utcTiming = null;
List periods = new ArrayList();
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl);
+ } else if (isStartTag(xpp, "UTCTiming")) {
+ utcTiming = parseUtcTiming(xpp);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
}
} while (!isEndTag(xpp, "MPD"));
- return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
- periods);
+ return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
+ dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods);
+ }
+
+ private UtcTimingElement parseUtcTiming(XmlPullParser xpp) {
+ String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
+ String value = xpp.getAttributeValue(null, "value");
+ return new UtcTimingElement(schemeIdUri, value);
}
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
@@ -429,6 +454,62 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return parseDurationMs(xpp, name, -1);
}
+ private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
+ throws ParseException {
+ String value = xpp.getAttributeValue(null, name);
+
+ if (value == null) {
+ return defaultValue;
+ } else {
+ return parseDateTime(value);
+ }
+ }
+
+ // VisibleForTesting
+ static long parseDateTime(String value) throws ParseException {
+ Matcher matcher = DATE_TIME_PATTERN.matcher(value);
+ if (!matcher.matches()) {
+ throw new ParseException("Invalid date/time format: " + value, 0);
+ }
+
+ int timezoneShift;
+ if (matcher.group(9) == null) {
+ // No time zone specified.
+ timezoneShift = 0;
+ } else if (matcher.group(9).equalsIgnoreCase("Z")) {
+ timezoneShift = 0;
+ } else {
+ timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60
+ + Integer.valueOf(matcher.group(13))));
+ if (matcher.group(11).equals("-")) {
+ timezoneShift *= -1;
+ }
+ }
+
+ Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+
+ dateTime.clear();
+ // Note: The month value is 0-based, hence the -1 on group(2)
+ dateTime.set(Integer.valueOf(matcher.group(1)),
+ Integer.valueOf(matcher.group(2)) - 1,
+ Integer.valueOf(matcher.group(3)),
+ Integer.valueOf(matcher.group(4)),
+ Integer.valueOf(matcher.group(5)),
+ Integer.valueOf(matcher.group(6)));
+ if (!TextUtils.isEmpty(matcher.group(8))) {
+ final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
+ // we care only for milliseconds, so movePointRight(3)
+ dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
+ }
+
+ long time = dateTime.getTimeInMillis();
+ if (timezoneShift != 0) {
+ time -= timezoneShift * 60000;
+ }
+
+ return time;
+ }
+
private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name);
if (value != null) {
diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java
new file mode 100644
index 0000000000..cbcc30de7e
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java
@@ -0,0 +1,31 @@
+/*
+ * 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.dash.mpd;
+
+/**
+ * Represents a UTCTiming element.
+ */
+public class UtcTimingElement {
+
+ public final String schemeIdUri;
+ public final String value;
+
+ public UtcTimingElement(String schemeIdUri, String value) {
+ this.schemeIdUri = schemeIdUri;
+ this.value = value;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/Extractor.java
new file mode 100644
index 0000000000..69db87ae42
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/parser/Extractor.java
@@ -0,0 +1,115 @@
+/*
+ * 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.parser;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 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 #getPsshInfo}.
+ */
+ 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 the pssh information parsed from the stream.
+ *
+ * @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
+ * does not contain any pssh data.
+ */
+ public Map getPsshInfo();
+
+ /**
+ * Consumes data from a {@link NonBlockingInputStream}.
+ *
+ * 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);
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
index 936bd565d0..3267d5b409 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/mp4/FragmentedMp4Extractor.java
@@ -18,11 +18,13 @@ package com.google.android.exoplayer.parser.mp4;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.SuppressLint;
@@ -47,7 +49,7 @@ import java.util.UUID;
*
* This implementation only supports de-muxed (i.e. single track) streams.
*/
-public final class FragmentedMp4Extractor {
+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.
@@ -58,32 +60,6 @@ public final class FragmentedMp4Extractor {
*/
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
- /**
- * 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;
- /**
- * A moov atom was read. The parsed data can be read using {@link #getFormat()} and
- * {@link #getPsshInfo}.
- */
- 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;
-
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[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
@@ -196,22 +172,13 @@ public final class FragmentedMp4Extractor {
}
/**
- * Returns the segment index parsed from the stream.
+ * Sideloads track information into the extractor.
*
- * @return The segment index, or null if a SIDX atom has yet to be parsed.
+ * @param track The track to sideload.
*/
- public SegmentIndex getIndex() {
- return segmentIndex;
- }
-
- /**
- * Returns the pssh information parsed from the stream.
- *
- * @return The pssh information. May be null if the MOOV atom has yet to be parsed of if it did
- * not contain any pssh information.
- */
- public Map getPsshInfo() {
- return psshData.isEmpty() ? null : psshData;
+ public void setTrack(Track track) {
+ this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
+ this.track = track;
}
/**
@@ -228,38 +195,27 @@ public final class FragmentedMp4Extractor {
psshData.put(uuid, data);
}
- /**
- * Returns the format of the samples contained within the media stream.
- *
- * @return The sample media format, or null if a MOOV atom has yet to be parsed.
- */
+ @Override
+ public Map getPsshInfo() {
+ return psshData.isEmpty() ? null : psshData;
+ }
+
+ @Override
+ public SegmentIndex getIndex() {
+ return segmentIndex;
+ }
+
+ @Override
+ public boolean hasRelativeIndexOffsets() {
+ return true;
+ }
+
+ @Override
public MediaFormat getFormat() {
return track == null ? null : track.mediaFormat;
}
- /**
- * 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;
- }
-
- /**
- * Consumes data from a {@link NonBlockingInputStream}.
- *
- * 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.
- */
+ @Override
public int read(NonBlockingInputStream inputStream, SampleHolder out)
throws ParserException {
try {
@@ -286,15 +242,7 @@ public final class FragmentedMp4Extractor {
}
}
- /**
- * 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.
- */
+ @Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
pendingSeekTimeMs = (int) (seekTimeUs / 1000);
if (allowNoop && fragmentRun != null
@@ -780,10 +728,10 @@ public final class FragmentedMp4Extractor {
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
- TrackEncryptionBox trackEncryptionBox =
- track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
+ TrackEncryptionBox trackEncryptionBox =
+ track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
parseSaiz(trackEncryptionBox, saiz.data, out);
}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
deleted file mode 100644
index 00f24bbad7..0000000000
--- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultWebmExtractor.java
+++ /dev/null
@@ -1,462 +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.parser.webm;
-
-import com.google.android.exoplayer.MediaFormat;
-import com.google.android.exoplayer.SampleHolder;
-import com.google.android.exoplayer.parser.SegmentIndex;
-import com.google.android.exoplayer.upstream.NonBlockingInputStream;
-import com.google.android.exoplayer.util.LongArray;
-import com.google.android.exoplayer.util.MimeTypes;
-
-import android.annotation.TargetApi;
-import android.media.MediaExtractor;
-
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Default version of an extractor to facilitate data retrieval from the WebM container format.
- *
- *
WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
- * Matroska is available here.
- * More info about WebM is here.
- */
-@TargetApi(16)
-public final class DefaultWebmExtractor implements WebmExtractor {
-
- private static final String DOC_TYPE_WEBM = "webm";
- private static final String CODEC_ID_VP9 = "V_VP9";
- 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_TRACKS = 0x1654AE6B;
- private static final int ID_TRACK_ENTRY = 0xAE;
- private static final int ID_CODEC_ID = 0x86;
- 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_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;
-
- // SimpleBlock Lacing Values
- private static final int LACING_NONE = 0;
- private static final int LACING_XIPH = 1;
- private static final int LACING_FIXED = 2;
- private static final int LACING_EBML = 3;
-
- 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 SampleHolder sampleHolder;
- private int readResults;
-
- private long segmentStartOffsetBytes = UNKNOWN;
- private long segmentEndOffsetBytes = UNKNOWN;
- private long timecodeScale = 1000000L;
- private long durationUs = UNKNOWN;
- private int pixelWidth = UNKNOWN;
- private int pixelHeight = UNKNOWN;
- 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 DefaultWebmExtractor() {
- this(new DefaultEbmlReader());
- }
-
- /* package */ DefaultWebmExtractor(EbmlReader reader) {
- this.reader = reader;
- this.reader.setEventHandler(new InnerEbmlEventHandler());
- }
-
- @Override
- public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
- 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 MediaFormat getFormat() {
- return format;
- }
-
- /* 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_VIDEO:
- case ID_CUES:
- case ID_CUE_POINT:
- case ID_CUE_TRACK_POSITIONS:
- 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_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_SIMPLE_BLOCK:
- return EbmlReader.TYPE_BINARY;
- case ID_DURATION:
- return EbmlReader.TYPE_FLOAT;
- default:
- return EbmlReader.TYPE_UNKNOWN;
- }
- }
-
- /* package */ boolean onMasterElementStart(
- int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
- switch (id) {
- case ID_SEGMENT:
- if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
- throw new IllegalStateException("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;
- default:
- // pass
- }
- return true;
- }
-
- /* package */ boolean onMasterElementEnd(int id) {
- switch (id) {
- case ID_CUES:
- buildCues();
- return false;
- case ID_VIDEO:
- buildFormat();
- return true;
- default:
- return true;
- }
- }
-
- /* package */ boolean onIntegerElement(int id, long value) {
- switch (id) {
- case ID_EBML_READ_VERSION:
- // Validate that EBMLReadVersion is supported. This extractor only supports v1.
- if (value != 1) {
- throw new IllegalArgumentException("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 IllegalArgumentException("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_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) {
- if (id == ID_DURATION) {
- durationUs = scaleTimecodeToUs((long) value);
- }
- return true;
- }
-
- /* package */ boolean onStringElement(int id, String value) {
- switch (id) {
- case ID_DOC_TYPE:
- // Validate that DocType is supported. This extractor only supports "webm".
- if (!DOC_TYPE_WEBM.equals(value)) {
- throw new IllegalArgumentException("DocType " + value + " not supported");
- }
- break;
- case ID_CODEC_ID:
- // Validate that CodecID is supported. This extractor only supports "V_VP9".
- if (!CODEC_ID_VP9.equals(value)) {
- throw new IllegalArgumentException("CodecID " + value + " not supported");
- }
- break;
- default:
- // pass
- }
- return true;
- }
-
- /* package */ boolean onBinaryElement(
- int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
- NonBlockingInputStream inputStream) {
- if (id == ID_SIMPLE_BLOCK) {
- // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
- // for info about how data is organized in a SimpleBlock element.
-
- // 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 = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
- boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
- int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
-
- // Validate lacing and set info into sample holder.
- switch (lacing) {
- case LACING_NONE:
- long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
- simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
- sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
- sampleHolder.decodeOnly = invisible;
- sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
- sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
- break;
- case LACING_EBML:
- case LACING_FIXED:
- case LACING_XIPH:
- default:
- throw new IllegalStateException("Lacing mode " + lacing + " not supported");
- }
-
- ByteBuffer outputData = sampleHolder.data;
- if (sampleHolder.allowDataBufferReplacement
- && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
- outputData = ByteBuffer.allocate(sampleHolder.size);
- sampleHolder.data = outputData;
- }
-
- if (outputData == null) {
- reader.skipBytes(inputStream, sampleHolder.size);
- sampleHolder.size = 0;
- } else {
- reader.readBytes(inputStream, outputData, sampleHolder.size);
- }
- readResults |= RESULT_READ_SAMPLE;
- }
- return true;
- }
-
- private long scaleTimecodeToUs(long unscaledTimecode) {
- return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
- }
-
- /**
- * Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
- *
- *
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 IllegalStateException} will be thrown.
- */
- private void buildFormat() {
- if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
- && (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
- format = MediaFormat.createVideoFormat(
- MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
- readResults |= RESULT_READ_INIT;
- } else if (format == null) {
- throw new IllegalStateException("Unable to build format");
- }
- }
-
- /**
- * Build a {@link SegmentIndex} containing recently gathered Cues information.
- *
- *
{@link #cues} is guaranteed to not be null after calling this method. In
- * the event that it can't be built, an {@link IllegalStateException} will be thrown.
- */
- private void buildCues() {
- if (segmentStartOffsetBytes == UNKNOWN) {
- throw new IllegalStateException("Segment start/end offsets unknown");
- } else if (durationUs == UNKNOWN) {
- throw new IllegalStateException("Duration unknown");
- } else if (cuesSizeBytes == UNKNOWN) {
- throw new IllegalStateException("Cues size unknown");
- } else if (cueTimesUs == null || cueClusterPositions == null
- || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
- throw new IllegalStateException("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;
- }
-
- /**
- * Passes events through to {@link DefaultWebmExtractor} as
- * callbacks from {@link EbmlReader} are received.
- */
- private final class InnerEbmlEventHandler implements EbmlEventHandler {
-
- @Override
- public int getElementType(int id) {
- return DefaultWebmExtractor.this.getElementType(id);
- }
-
- @Override
- public void onMasterElementStart(
- int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
- DefaultWebmExtractor.this.onMasterElementStart(
- id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
- }
-
- @Override
- public void onMasterElementEnd(int id) {
- DefaultWebmExtractor.this.onMasterElementEnd(id);
- }
-
- @Override
- public void onIntegerElement(int id, long value) {
- DefaultWebmExtractor.this.onIntegerElement(id, value);
- }
-
- @Override
- public void onFloatElement(int id, double value) {
- DefaultWebmExtractor.this.onFloatElement(id, value);
- }
-
- @Override
- public void onStringElement(int id, String value) {
- DefaultWebmExtractor.this.onStringElement(id, value);
- }
-
- @Override
- public boolean onBinaryElement(
- int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
- NonBlockingInputStream inputStream) {
- return DefaultWebmExtractor.this.onBinaryElement(
- id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
- }
-
- }
-
-}
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
index e824887476..a0e0b962b3 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
+++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/WebmExtractor.java
@@ -17,76 +17,460 @@ package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.LongArray;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.annotation.TargetApi;
+import android.media.MediaExtractor;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
/**
- * Extractor to facilitate data retrieval from the WebM container format.
+ * An extractor to facilitate data retrieval from the WebM container format.
*
*
WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available here.
* More info about WebM is here.
*/
-public interface WebmExtractor {
+@TargetApi(16)
+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 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_TRACKS = 0x1654AE6B;
+ private static final int ID_TRACK_ENTRY = 0xAE;
+ private static final int ID_CODEC_ID = 0x86;
+ 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_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;
+
+ // SimpleBlock Lacing Values
+ private static final int LACING_NONE = 0;
+ private static final int LACING_XIPH = 1;
+ private static final int LACING_FIXED = 2;
+ private static final int LACING_EBML = 3;
+
+ 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 SampleHolder sampleHolder;
+ private int readResults;
+
+ private long segmentStartOffsetBytes = UNKNOWN;
+ private long segmentEndOffsetBytes = UNKNOWN;
+ private long timecodeScale = 1000000L;
+ private long durationUs = UNKNOWN;
+ private int pixelWidth = UNKNOWN;
+ private int pixelHeight = UNKNOWN;
+ 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) {
+ 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 Map getPsshInfo() {
+ // TODO: Parse pssh data from Webm streams.
+ return null;
+ }
+
+ /* 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_VIDEO:
+ case ID_CUES:
+ case ID_CUE_POINT:
+ case ID_CUE_TRACK_POSITIONS:
+ 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_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_SIMPLE_BLOCK:
+ return EbmlReader.TYPE_BINARY;
+ case ID_DURATION:
+ return EbmlReader.TYPE_FLOAT;
+ default:
+ return EbmlReader.TYPE_UNKNOWN;
+ }
+ }
+
+ /* package */ boolean onMasterElementStart(
+ int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
+ switch (id) {
+ case ID_SEGMENT:
+ if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
+ throw new IllegalStateException("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;
+ default:
+ // pass
+ }
+ return true;
+ }
+
+ /* package */ boolean onMasterElementEnd(int id) {
+ switch (id) {
+ case ID_CUES:
+ buildCues();
+ return false;
+ case ID_VIDEO:
+ buildFormat();
+ return true;
+ default:
+ return true;
+ }
+ }
+
+ /* package */ boolean onIntegerElement(int id, long value) {
+ switch (id) {
+ case ID_EBML_READ_VERSION:
+ // Validate that EBMLReadVersion is supported. This extractor only supports v1.
+ if (value != 1) {
+ throw new IllegalArgumentException("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 IllegalArgumentException("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_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) {
+ if (id == ID_DURATION) {
+ durationUs = scaleTimecodeToUs((long) value);
+ }
+ return true;
+ }
+
+ /* package */ boolean onStringElement(int id, String value) {
+ switch (id) {
+ case ID_DOC_TYPE:
+ // Validate that DocType is supported. This extractor only supports "webm".
+ if (!DOC_TYPE_WEBM.equals(value)) {
+ throw new IllegalArgumentException("DocType " + value + " not supported");
+ }
+ break;
+ case ID_CODEC_ID:
+ // Validate that CodecID is supported. This extractor only supports "V_VP9".
+ if (!CODEC_ID_VP9.equals(value)) {
+ throw new IllegalArgumentException("CodecID " + value + " not supported");
+ }
+ break;
+ default:
+ // pass
+ }
+ return true;
+ }
+
+ /* package */ boolean onBinaryElement(
+ int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+ NonBlockingInputStream inputStream) {
+ if (id == ID_SIMPLE_BLOCK) {
+ // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+ // for info about how data is organized in a SimpleBlock element.
+
+ // 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 = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
+ boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
+ int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
+
+ // Validate lacing and set info into sample holder.
+ switch (lacing) {
+ case LACING_NONE:
+ long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
+ simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
+ sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
+ sampleHolder.decodeOnly = invisible;
+ sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
+ sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
+ break;
+ case LACING_EBML:
+ case LACING_FIXED:
+ case LACING_XIPH:
+ default:
+ throw new IllegalStateException("Lacing mode " + lacing + " not supported");
+ }
+
+ ByteBuffer outputData = sampleHolder.data;
+ if (sampleHolder.allowDataBufferReplacement
+ && (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
+ outputData = ByteBuffer.allocate(sampleHolder.size);
+ sampleHolder.data = outputData;
+ }
+
+ if (outputData == null) {
+ reader.skipBytes(inputStream, sampleHolder.size);
+ sampleHolder.size = 0;
+ } else {
+ reader.readBytes(inputStream, outputData, sampleHolder.size);
+ }
+ readResults |= RESULT_READ_SAMPLE;
+ }
+ return true;
+ }
+
+ private long scaleTimecodeToUs(long unscaledTimecode) {
+ return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
+ }
/**
- * 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()}.
- */
- 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;
-
- /**
- * Consumes data from a {@link NonBlockingInputStream}.
+ * Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
*
- * @param inputStream The input stream from which data should be read
- * @param sampleHolder A {@link SampleHolder} into which the sample should be read
- * @return One or more of the {@code RESULT_*} flags defined in this class.
+ * 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 IllegalStateException} will be thrown.
*/
- public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
+ private void buildFormat() {
+ if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
+ && (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
+ format = MediaFormat.createVideoFormat(
+ MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
+ readResults |= RESULT_READ_INIT;
+ } else if (format == null) {
+ throw new IllegalStateException("Unable to build format");
+ }
+ }
/**
- * Seeks to a position before or equal to the requested time.
+ * Build a {@link SegmentIndex} containing recently gathered Cues information.
*
- * @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
- * segment, 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
+ *
{@link #cues} is guaranteed to not be null after calling this method. In
+ * the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
- public boolean seekTo(long seekTimeUs, boolean allowNoop);
+ private void buildCues() {
+ if (segmentStartOffsetBytes == UNKNOWN) {
+ throw new IllegalStateException("Segment start/end offsets unknown");
+ } else if (durationUs == UNKNOWN) {
+ throw new IllegalStateException("Duration unknown");
+ } else if (cuesSizeBytes == UNKNOWN) {
+ throw new IllegalStateException("Cues size unknown");
+ } else if (cueTimesUs == null || cueClusterPositions == null
+ || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
+ throw new IllegalStateException("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;
+ }
/**
- * Returns the cues for the media stream.
- *
- * @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
- * prepared
+ * Passes events through to {@link WebmExtractor} as
+ * callbacks from {@link EbmlReader} are received.
*/
- public SegmentIndex getIndex();
+ private final class InnerEbmlEventHandler implements EbmlEventHandler {
- /**
- * Returns the format of the samples contained within the media stream.
- *
- * @return The sample media format, or null if the extracted is not yet prepared
- */
- public MediaFormat getFormat();
+ @Override
+ public int getElementType(int id) {
+ return WebmExtractor.this.getElementType(id);
+ }
+
+ @Override
+ public void onMasterElementStart(
+ int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
+ WebmExtractor.this.onMasterElementStart(
+ id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
+ }
+
+ @Override
+ public void onMasterElementEnd(int id) {
+ WebmExtractor.this.onMasterElementEnd(id);
+ }
+
+ @Override
+ public void onIntegerElement(int id, long value) {
+ 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) {
+ WebmExtractor.this.onStringElement(id, value);
+ }
+
+ @Override
+ public boolean onBinaryElement(
+ int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+ NonBlockingInputStream inputStream) {
+ return WebmExtractor.this.onBinaryElement(
+ id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
+ }
+
+ }
}
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
index d6a70e0364..918ddb4f90 100644
--- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java
@@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
-import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
+import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.parser.mp4.Track;
import com.google.android.exoplayer.parser.mp4.TrackEncryptionBox;
@@ -35,6 +35,7 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import android.net.Uri;
import android.util.Base64;
@@ -227,7 +228,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
- FragmentedMp4Extractor extractor, DataSource dataSource, int chunkIndex,
+ Extractor extractor, DataSource dataSource, int chunkIndex,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java
index f19a054346..8fb6e66e40 100644
--- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java
+++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java
@@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
-import org.xmlpull.v1.XmlPullParserException;
-
import java.io.IOException;
import java.io.InputStream;
@@ -59,11 +57,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher 0) {
+ skippingElementDepth++;
+ } else if (handleChildInline(tagName)) {
parseStartTag(xmlParser);
} else {
- addChild(newChildParser(this, tagName).parse(xmlParser));
+ ElementParser childElementParser = newChildParser(this, tagName);
+ if (childElementParser == null) {
+ skippingElementDepth = 1;
+ } else {
+ addChild(childElementParser.parse(xmlParser));
+ }
}
}
break;
case XmlPullParser.TEXT:
- if (foundStartTag) {
+ if (foundStartTag && skippingElementDepth == 0) {
parseText(xmlParser);
}
break;
case XmlPullParser.END_TAG:
if (foundStartTag) {
- tagName = xmlParser.getName();
- parseEndTag(xmlParser);
- if (!handleChildInline(tagName)) {
- return build();
+ if (skippingElementDepth > 0) {
+ skippingElementDepth--;
+ } else {
+ tagName = xmlParser.getName();
+ parseEndTag(xmlParser);
+ if (!handleChildInline(tagName)) {
+ return build();
+ }
}
}
break;
@@ -357,6 +372,7 @@ public class SmoothStreamingManifestParser {
public static final String KEY_SYSTEM_ID = "SystemID";
+ private boolean inProtectionHeader;
private UUID uuid;
private byte[] initData;
@@ -371,16 +387,25 @@ public class SmoothStreamingManifestParser {
@Override
public void parseStartTag(XmlPullParser parser) {
- if (!TAG_PROTECTION_HEADER.equals(parser.getName())) {
- return;
+ if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
+ inProtectionHeader = true;
+ String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
+ uuid = UUID.fromString(uuidString);
}
- String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
- uuid = UUID.fromString(uuidString);
}
@Override
public void parseText(XmlPullParser parser) {
- initData = Base64.decode(parser.getText(), Base64.DEFAULT);
+ if (inProtectionHeader) {
+ initData = Base64.decode(parser.getText(), Base64.DEFAULT);
+ }
+ }
+
+ @Override
+ public void parseEndTag(XmlPullParser parser) {
+ if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
+ inProtectionHeader = false;
+ }
}
@Override
@@ -579,9 +604,11 @@ public class SmoothStreamingManifestParser {
if (type == StreamElement.TYPE_VIDEO) {
maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
+ fourCC = parseRequiredString(parser, KEY_FOUR_CC);
} else {
maxHeight = -1;
maxWidth = -1;
+ fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
}
if (type == StreamElement.TYPE_AUDIO) {
@@ -590,14 +617,12 @@ public class SmoothStreamingManifestParser {
bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
- fourCC = parseRequiredString(parser, KEY_FOUR_CC);
} else {
samplingRate = -1;
channels = -1;
bitPerSample = -1;
packetSize = -1;
audioTag = -1;
- fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
}
value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java
index 1fd213eda1..d6504461b2 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java
@@ -16,7 +16,7 @@
package com.google.android.exoplayer.text;
import com.google.android.exoplayer.ExoPlaybackException;
-import com.google.android.exoplayer.FormatHolder;
+import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
@@ -64,7 +64,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
private final TextRenderer textRenderer;
private final SampleSource source;
private final SampleHolder sampleHolder;
- private final FormatHolder formatHolder;
+ private final MediaFormatHolder formatHolder;
private final SubtitleParser subtitleParser;
private int trackIndex;
@@ -93,7 +93,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
this);
- formatHolder = new FormatHolder();
+ formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(true);
}
diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java
index 379b63fa53..a80df7fb86 100644
--- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java
+++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java
@@ -41,10 +41,10 @@ public final class CacheDataSource implements DataSource {
public interface EventListener {
/**
- * Invoked when bytes have been read from {@link #cache} since the last invocation.
+ * Invoked when bytes have been read from the cache.
*
* @param cacheSizeBytes Current cache size in bytes.
- * @param cachedBytesRead Total bytes read from {@link #cache} since last report.
+ * @param cachedBytesRead Total bytes read from the cache since this method was last invoked.
*/
void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
diff --git a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java b/library/src/main/java/com/google/android/exoplayer/util/CodecSpecificDataUtil.java
similarity index 86%
rename from library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java
rename to library/src/main/java/com/google/android/exoplayer/util/CodecSpecificDataUtil.java
index 851c4925b6..019f7459c0 100644
--- a/library/src/main/java/com/google/android/exoplayer/parser/mp4/CodecSpecificDataUtil.java
+++ b/library/src/main/java/com/google/android/exoplayer/util/CodecSpecificDataUtil.java
@@ -13,9 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.android.exoplayer.parser.mp4;
-
-import com.google.android.exoplayer.util.Assertions;
+package com.google.android.exoplayer.util;
import android.annotation.SuppressLint;
import android.media.MediaCodecInfo.CodecProfileLevel;
@@ -35,6 +33,10 @@ public final class CodecSpecificDataUtil {
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
};
+ private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE = new int[] {
+ 0, 1, 2, 3, 4, 5, 6, 8
+ };
+
private static final int SPS_NAL_UNIT_TYPE = 7;
private CodecSpecificDataUtil() {}
@@ -42,7 +44,7 @@ public final class CodecSpecificDataUtil {
/**
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
*
- * @param audioSpecificConfig
+ * @param audioSpecificConfig The AudioSpecificConfig to parse.
* @return A pair consisting of the sample rate in Hz and the channel count.
*/
public static Pair parseAudioSpecificConfig(byte[] audioSpecificConfig) {
@@ -56,11 +58,27 @@ public final class CodecSpecificDataUtil {
return Pair.create(sampleRate, channelCount);
}
+ /**
+ * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+ *
+ * @param audioObjectType The audio object type.
+ * @param sampleRateIndex The sample rate index.
+ * @param channelConfig The channel configuration.
+ * @return The AudioSpecificConfig.
+ */
+ public static byte[] buildAudioSpecificConfig(int audioObjectType, int sampleRateIndex,
+ int channelConfig) {
+ byte[] audioSpecificConfig = new byte[2];
+ audioSpecificConfig[0] = (byte) ((audioObjectType << 3) & 0xF8 | (sampleRateIndex >> 1) & 0x07);
+ audioSpecificConfig[1] = (byte) ((sampleRateIndex << 7) & 0x80 | (channelConfig << 3) & 0x78);
+ return audioSpecificConfig;
+ }
+
/**
* Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
*
* @param sampleRate The sample rate in Hz.
- * @param numChannels The number of channels
+ * @param numChannels The number of channels.
* @return The AudioSpecificConfig.
*/
public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
@@ -70,10 +88,16 @@ public final class CodecSpecificDataUtil {
sampleRateIndex = i;
}
}
+ int channelConfig = -1;
+ for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) {
+ if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {
+ channelConfig = i;
+ }
+ }
// The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1
byte[] csd = new byte[2];
csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1));
- csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (numChannels << 3));
+ csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (channelConfig << 3));
return csd;
}