commit
280ccb1630
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.google.android.exoplayer.demo"
|
package="com.google.android.exoplayer.demo"
|
||||||
android:versionCode="1012"
|
android:versionCode="1013"
|
||||||
android:versionName="1.0.12"
|
android:versionName="1.0.13"
|
||||||
android:theme="@style/RootTheme">
|
android:theme="@style/RootTheme">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
@ -134,6 +134,8 @@ package com.google.android.exoplayer.demo;
|
|||||||
public static final Sample[] MISC = new Sample[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
|
||||||
DemoUtil.TYPE_OTHER, false, true),
|
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() {}
|
private Samples() {}
|
||||||
|
@ -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;
|
||||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||||
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
||||||
import com.google.android.exoplayer.dash.DashMp4ChunkSource;
|
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||||
import com.google.android.exoplayer.dash.DashWebmChunkSource;
|
|
||||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
||||||
@ -163,14 +162,8 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
|||||||
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
ChunkSource videoChunkSource;
|
ChunkSource videoChunkSource;
|
||||||
String mimeType = videoRepresentations[0].format.mimeType;
|
String mimeType = videoRepresentations[0].format.mimeType;
|
||||||
if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
|
if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
||||||
videoChunkSource = new DashMp4ChunkSource(videoDataSource,
|
videoChunkSource = new DashChunkSource(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,
|
|
||||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unexpected mime type: " + mimeType);
|
throw new IllegalStateException("Unexpected mime type: " + mimeType);
|
||||||
@ -200,7 +193,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
|||||||
Format format = representation.format;
|
Format format = representation.format;
|
||||||
audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " +
|
audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " +
|
||||||
format.audioSamplingRate + "Hz)";
|
format.audioSamplingRate + "Hz)";
|
||||||
audioChunkSources[i] = new DashMp4ChunkSource(audioDataSource,
|
audioChunkSources[i] = new DashChunkSource(audioDataSource,
|
||||||
audioEvaluator, representation);
|
audioEvaluator, representation);
|
||||||
}
|
}
|
||||||
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
|
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
|
||||||
|
@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.ChunkSource;
|
|||||||
import com.google.android.exoplayer.chunk.Format;
|
import com.google.android.exoplayer.chunk.Format;
|
||||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
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.AdaptationSet;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
||||||
@ -116,7 +116,7 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
|
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
|
||||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||||
@ -125,7 +125,7 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
// Build the audio renderer.
|
// Build the audio renderer.
|
||||||
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
|
ChunkSource audioChunkSource = new DashChunkSource(audioDataSource,
|
||||||
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
|
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
|
||||||
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||||
|
@ -21,6 +21,12 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Important: ExoPlayerLib specifies a minSdkVersion of 9 because various components provided by
|
||||||
|
the library may be of use on older devices. However, please note that the core video playback
|
||||||
|
functionality provided by the library requires API level 16 or greater.
|
||||||
|
-->
|
||||||
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
|
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
|
|||||||
/**
|
/**
|
||||||
* The version of the library, expressed as a string.
|
* 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.
|
* 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
|
* Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
|
||||||
* corresponding integer version 1002003.
|
* 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}
|
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
|
||||||
|
@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
|
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
|
||||||
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
|
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
|
||||||
Assertions.checkState(prepared);
|
Assertions.checkState(prepared);
|
||||||
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
|
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
|
||||||
|
@ -27,6 +27,7 @@ import android.media.AudioTimestamp;
|
|||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.media.audiofx.Virtualizer;
|
||||||
import android.os.ConditionVariable;
|
import android.os.ConditionVariable;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@ -91,6 +92,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
|
|
||||||
private static final long MICROS_PER_SECOND = 1000000L;
|
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 MAX_PLAYHEAD_OFFSET_COUNT = 10;
|
||||||
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
|
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
|
||||||
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
|
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
|
||||||
@ -358,9 +366,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
* subsequently re-enabled.
|
* subsequently re-enabled.
|
||||||
* <p>
|
* <p>
|
||||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
* 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
|
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
|
||||||
* audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances
|
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
|
||||||
* should be released in {@link #onDisabled()} (if not before).
|
* (if not before).
|
||||||
*
|
*
|
||||||
* @param audioSessionId The audio session id.
|
* @param audioSessionId The audio session id.
|
||||||
*/
|
*/
|
||||||
@ -425,7 +433,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isReady() {
|
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) {
|
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
|
||||||
audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
|
audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
|
||||||
if (audioTimestampSet
|
if (audioTimestampSet) {
|
||||||
&& (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) {
|
// Perform sanity checks on the timestamp.
|
||||||
// The timestamp was set, but it corresponds to a time before the track was most recently
|
long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000;
|
||||||
// resumed.
|
if (audioTimestampUs < audioTrackResumeSystemTimeUs) {
|
||||||
|
// The timestamp corresponds to a time before the track was most recently resumed.
|
||||||
audioTimestampSet = false;
|
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) {
|
if (audioTrackGetLatencyMethod != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -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
|
* 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
|
* 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 boolean playClearSamplesWithoutKeys;
|
||||||
private final SampleSource source;
|
private final SampleSource source;
|
||||||
private final SampleHolder sampleHolder;
|
private final SampleHolder sampleHolder;
|
||||||
private final FormatHolder formatHolder;
|
private final MediaFormatHolder formatHolder;
|
||||||
private final HashSet<Long> decodeOnlyPresentationTimestamps;
|
private final HashSet<Long> decodeOnlyPresentationTimestamps;
|
||||||
private final MediaCodec.BufferInfo outputBufferInfo;
|
private final MediaCodec.BufferInfo outputBufferInfo;
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
@ -128,7 +144,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
private int codecReconfigurationState;
|
private int codecReconfigurationState;
|
||||||
|
|
||||||
private int trackIndex;
|
private int trackIndex;
|
||||||
private boolean sourceIsReady;
|
private int sourceState;
|
||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
private boolean outputStreamEnded;
|
private boolean outputStreamEnded;
|
||||||
private boolean waitingForKeys;
|
private boolean waitingForKeys;
|
||||||
@ -158,7 +174,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
codecCounters = new CodecCounters();
|
codecCounters = new CodecCounters();
|
||||||
sampleHolder = new SampleHolder(false);
|
sampleHolder = new SampleHolder(false);
|
||||||
formatHolder = new FormatHolder();
|
formatHolder = new MediaFormatHolder();
|
||||||
decodeOnlyPresentationTimestamps = new HashSet<Long>();
|
decodeOnlyPresentationTimestamps = new HashSet<Long>();
|
||||||
outputBufferInfo = new MediaCodec.BufferInfo();
|
outputBufferInfo = new MediaCodec.BufferInfo();
|
||||||
}
|
}
|
||||||
@ -202,7 +218,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
@Override
|
@Override
|
||||||
protected void onEnabled(long timeUs, boolean joining) {
|
protected void onEnabled(long timeUs, boolean joining) {
|
||||||
source.enable(trackIndex, timeUs);
|
source.enable(trackIndex, timeUs);
|
||||||
sourceIsReady = false;
|
sourceState = SOURCE_STATE_NOT_READY;
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
@ -353,7 +369,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
protected void seekTo(long timeUs) throws ExoPlaybackException {
|
||||||
currentPositionUs = timeUs;
|
currentPositionUs = timeUs;
|
||||||
source.seekToUs(timeUs);
|
source.seekToUs(timeUs);
|
||||||
sourceIsReady = false;
|
sourceState = SOURCE_STATE_NOT_READY;
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
@ -372,7 +388,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
@Override
|
@Override
|
||||||
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
|
||||||
try {
|
try {
|
||||||
sourceIsReady = source.continueBuffering(timeUs);
|
sourceState = source.continueBuffering(timeUs)
|
||||||
|
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
|
||||||
|
: SOURCE_STATE_NOT_READY;
|
||||||
checkForDiscontinuity();
|
checkForDiscontinuity();
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
readFormat();
|
readFormat();
|
||||||
@ -384,7 +402,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
}
|
}
|
||||||
if (codec != null) {
|
if (codec != null) {
|
||||||
while (drainOutputBuffer(timeUs)) {}
|
while (drainOutputBuffer(timeUs)) {}
|
||||||
while (feedInputBuffer()) {}
|
if (feedInputBuffer(true)) {
|
||||||
|
while (feedInputBuffer(false)) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
codecCounters.ensureUpdated();
|
codecCounters.ensureUpdated();
|
||||||
@ -429,6 +449,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
codecHotswapTimeMs = -1;
|
codecHotswapTimeMs = -1;
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
outputIndex = -1;
|
outputIndex = -1;
|
||||||
|
waitingForFirstSyncFrame = true;
|
||||||
decodeOnlyPresentationTimestamps.clear();
|
decodeOnlyPresentationTimestamps.clear();
|
||||||
// Workaround for framework bugs.
|
// Workaround for framework bugs.
|
||||||
// See [redacted], [redacted], [redacted].
|
// 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.
|
* @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 IOException If an error occurs reading data from the upstream source.
|
||||||
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
* @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) {
|
if (inputStreamEnded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -478,6 +501,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
|
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
|
||||||
}
|
}
|
||||||
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
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) {
|
if (result == SampleSource.NOTHING_READ) {
|
||||||
@ -594,7 +620,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
* @param formatHolder Holds the new format.
|
* @param formatHolder Holds the new format.
|
||||||
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
|
* @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;
|
MediaFormat oldFormat = format;
|
||||||
format = formatHolder.format;
|
format = formatHolder.format;
|
||||||
drmInitData = formatHolder.drmInitData;
|
drmInitData = formatHolder.drmInitData;
|
||||||
@ -646,7 +672,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean isReady() {
|
protected boolean isReady() {
|
||||||
return format != null && !waitingForKeys
|
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() {
|
private boolean isWithinHotswapPeriod() {
|
||||||
|
@ -235,7 +235,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean isReady() {
|
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.
|
// Ready. If we were joining then we've now joined, so clear the joining deadline.
|
||||||
joiningDeadlineUs = -1;
|
joiningDeadlineUs = -1;
|
||||||
return true;
|
return true;
|
||||||
|
@ -27,7 +27,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
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 {
|
public class MediaFormat {
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import java.util.UUID;
|
|||||||
/**
|
/**
|
||||||
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
|
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
|
||||||
*/
|
*/
|
||||||
public final class FormatHolder {
|
public final class MediaFormatHolder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The format of the media.
|
* The format of the media.
|
@ -80,7 +80,7 @@ public interface SampleSource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable the specified track. This allows the track's format and samples to be read from
|
* 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)}.
|
||||||
* <p>
|
* <p>
|
||||||
* This method should not be called until after the source has been successfully prepared.
|
* 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 track The track from which to read.
|
||||||
* @param playbackPositionUs The current playback position.
|
* @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
|
* @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}
|
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
|
||||||
* references a valid output buffer.
|
* references a valid output buffer.
|
||||||
@ -129,7 +129,7 @@ public interface SampleSource {
|
|||||||
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
|
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
|
||||||
* @throws IOException If an error occurred reading from the source.
|
* @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;
|
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
package com.google.android.exoplayer.chunk;
|
package com.google.android.exoplayer.chunk;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.FormatHolder;
|
|
||||||
import com.google.android.exoplayer.LoadControl;
|
import com.google.android.exoplayer.LoadControl;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
|
import com.google.android.exoplayer.MediaFormatHolder;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackInfo;
|
import com.google.android.exoplayer.TrackInfo;
|
||||||
@ -267,7 +267,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
|
||||||
Assertions.checkState(state == STATE_ENABLED);
|
Assertions.checkState(state == STATE_ENABLED);
|
||||||
Assertions.checkState(track == 0);
|
Assertions.checkState(track == 0);
|
||||||
@ -318,6 +318,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaChunk.prepare()) {
|
if (!mediaChunk.prepare()) {
|
||||||
|
if (currentLoadableException != null) {
|
||||||
|
throw currentLoadableException;
|
||||||
|
}
|
||||||
return NOTHING_READ;
|
return NOTHING_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A format definition for streams.
|
* Defines the high level format of a media stream.
|
||||||
*/
|
*/
|
||||||
public class Format {
|
public class Format {
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ package com.google.android.exoplayer.chunk;
|
|||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
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.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
@ -32,7 +32,7 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
public final class Mp4MediaChunk extends MediaChunk {
|
public final class Mp4MediaChunk extends MediaChunk {
|
||||||
|
|
||||||
private final FragmentedMp4Extractor extractor;
|
private final Extractor extractor;
|
||||||
private final boolean maybeSelfContained;
|
private final boolean maybeSelfContained;
|
||||||
private final long sampleOffsetUs;
|
private final long sampleOffsetUs;
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
|||||||
*/
|
*/
|
||||||
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||||
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
|
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);
|
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
this.maybeSelfContained = maybeSelfContained;
|
this.maybeSelfContained = maybeSelfContained;
|
||||||
@ -89,7 +89,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
|||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
Assertions.checkState(inputStream != null);
|
Assertions.checkState(inputStream != null);
|
||||||
int result = extractor.read(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 {
|
} else {
|
||||||
// We know there isn't a moov atom. The extractor must have parsed one from a separate
|
// We know there isn't a moov atom. The extractor must have parsed one from a separate
|
||||||
// initialization chunk.
|
// initialization chunk.
|
||||||
@ -107,7 +107,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
|||||||
public boolean sampleAvailable() throws ParserException {
|
public boolean sampleAvailable() throws ParserException {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
int result = extractor.read(inputStream, null);
|
int result = extractor.read(inputStream, null);
|
||||||
return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -115,7 +115,7 @@ public final class Mp4MediaChunk extends MediaChunk {
|
|||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
Assertions.checkState(inputStream != null);
|
Assertions.checkState(inputStream != null);
|
||||||
int result = extractor.read(inputStream, holder);
|
int result = extractor.read(inputStream, holder);
|
||||||
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
|
boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0;
|
||||||
if (sampleRead) {
|
if (sampleRead) {
|
||||||
holder.timeUs -= sampleOffsetUs;
|
holder.timeUs -= sampleOffsetUs;
|
||||||
}
|
}
|
||||||
|
@ -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<UUID, byte[]> getPsshInfo() {
|
|
||||||
// TODO: Add support for Pssh to WebmExtractor
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -29,10 +29,13 @@ import com.google.android.exoplayer.chunk.MediaChunk;
|
|||||||
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
||||||
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||||
|
import com.google.android.exoplayer.parser.Extractor;
|
||||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
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.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
@ -42,9 +45,11 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link ChunkSource} for Mp4 DASH streams.
|
* An {@link ChunkSource} for DASH streams.
|
||||||
|
* <p>
|
||||||
|
* This implementation currently supports fMP4 and webm.
|
||||||
*/
|
*/
|
||||||
public class DashMp4ChunkSource implements ChunkSource {
|
public class DashChunkSource implements ChunkSource {
|
||||||
|
|
||||||
private final TrackInfo trackInfo;
|
private final TrackInfo trackInfo;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
@ -55,7 +60,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
private final Format[] formats;
|
private final Format[] formats;
|
||||||
private final HashMap<String, Representation> representations;
|
private final HashMap<String, Representation> representations;
|
||||||
private final HashMap<String, FragmentedMp4Extractor> extractors;
|
private final HashMap<String, Extractor> extractors;
|
||||||
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
@ -65,12 +70,12 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
* @param evaluator Selects from the available formats.
|
* @param evaluator Selects from the available formats.
|
||||||
* @param representations The representations to be considered by the source.
|
* @param representations The representations to be considered by the source.
|
||||||
*/
|
*/
|
||||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||||
Representation... representations) {
|
Representation... representations) {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = evaluator;
|
this.evaluator = evaluator;
|
||||||
this.formats = new Format[representations.length];
|
this.formats = new Format[representations.length];
|
||||||
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
|
this.extractors = new HashMap<String, Extractor>();
|
||||||
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||||
this.representations = new HashMap<String, Representation>();
|
this.representations = new HashMap<String, Representation>();
|
||||||
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||||
@ -82,7 +87,9 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
formats[i] = representations[i].format;
|
formats[i] = representations[i].format;
|
||||||
maxWidth = Math.max(formats[i].width, maxWidth);
|
maxWidth = Math.max(formats[i].width, maxWidth);
|
||||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
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]);
|
this.representations.put(formats[i].id, representations[i]);
|
||||||
DashSegmentIndex segmentIndex = representations[i].getIndex();
|
DashSegmentIndex segmentIndex = representations[i].getIndex();
|
||||||
if (segmentIndex != null) {
|
if (segmentIndex != null) {
|
||||||
@ -142,7 +149,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||||
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||||
|
|
||||||
RangedUri pendingInitializationUri = null;
|
RangedUri pendingInitializationUri = null;
|
||||||
RangedUri pendingIndexUri = null;
|
RangedUri pendingIndexUri = null;
|
||||||
@ -191,35 +198,39 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||||
Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
|
Representation representation, Extractor extractor, DataSource dataSource,
|
||||||
int trigger) {
|
int trigger) {
|
||||||
int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM;
|
int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM;
|
||||||
long indexAnchor = 0;
|
long indexAnchor = 0;
|
||||||
RangedUri requestUri;
|
RangedUri requestUri;
|
||||||
if (initializationUri != null) {
|
if (initializationUri != null) {
|
||||||
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
||||||
// the two requests together to request both at once.
|
// the two requests together to request both at once.
|
||||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
|
expectedExtractorResult |= Extractor.RESULT_READ_INIT;
|
||||||
requestUri = initializationUri.attemptMerge(indexUri);
|
requestUri = initializationUri.attemptMerge(indexUri);
|
||||||
if (requestUri != null) {
|
if (requestUri != null) {
|
||||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
|
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
|
||||||
|
if (extractor.hasRelativeIndexOffsets()) {
|
||||||
indexAnchor = indexUri.start + indexUri.length;
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
requestUri = initializationUri;
|
requestUri = initializationUri;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
requestUri = indexUri;
|
requestUri = indexUri;
|
||||||
|
if (extractor.hasRelativeIndexOffsets()) {
|
||||||
indexAnchor = indexUri.start + indexUri.length;
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
|
}
|
||||||
|
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
|
||||||
}
|
}
|
||||||
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
|
return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
|
||||||
extractor, expectedExtractorResult, indexAnchor);
|
extractor, expectedExtractorResult, indexAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
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 lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
||||||
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
||||||
@ -232,15 +243,15 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
endTimeUs, nextSegmentNum, extractor, false, 0);
|
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 int expectedExtractorResult;
|
||||||
private final long indexAnchor;
|
private final long indexAnchor;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
|
||||||
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||||
Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
|
Format format, Extractor extractor, int expectedExtractorResult,
|
||||||
long indexAnchor) {
|
long indexAnchor) {
|
||||||
super(dataSource, dataSpec, format, trigger);
|
super(dataSource, dataSpec, format, trigger);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
@ -256,7 +267,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
throw new ParserException("Invalid extractor result. Expected "
|
throw new ParserException("Invalid extractor result. Expected "
|
||||||
+ expectedExtractorResult + ", got " + result);
|
+ expectedExtractorResult + ", got " + result);
|
||||||
}
|
}
|
||||||
if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
|
if ((result & Extractor.RESULT_READ_INDEX) != 0) {
|
||||||
segmentIndexes.put(format.id,
|
segmentIndexes.put(format.id,
|
||||||
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
|
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
|
||||||
}
|
}
|
@ -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<String, Representation> representations;
|
|
||||||
private final HashMap<String, DefaultWebmExtractor> extractors;
|
|
||||||
private final HashMap<String, DashSegmentIndex> 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<String, DefaultWebmExtractor>();
|
|
||||||
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
|
||||||
this.representations = new HashMap<String, Representation>();
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -23,6 +23,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class MediaPresentationDescription {
|
public final class MediaPresentationDescription {
|
||||||
|
|
||||||
|
public final long availabilityStartTime;
|
||||||
|
|
||||||
public final long duration;
|
public final long duration;
|
||||||
|
|
||||||
public final long minBufferTime;
|
public final long minBufferTime;
|
||||||
@ -31,14 +33,22 @@ public final class MediaPresentationDescription {
|
|||||||
|
|
||||||
public final long minUpdatePeriod;
|
public final long minUpdatePeriod;
|
||||||
|
|
||||||
|
public final long timeShiftBufferDepth;
|
||||||
|
|
||||||
public final List<Period> periods;
|
public final List<Period> periods;
|
||||||
|
|
||||||
public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic,
|
public final UtcTimingElement utcTiming;
|
||||||
long minUpdatePeriod, List<Period> periods) {
|
|
||||||
|
public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
|
||||||
|
boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
|
||||||
|
List<Period> periods) {
|
||||||
|
this.availabilityStartTime = availabilityStartTime;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.minBufferTime = minBufferTime;
|
this.minBufferTime = minBufferTime;
|
||||||
this.dynamic = dynamic;
|
this.dynamic = dynamic;
|
||||||
this.minUpdatePeriod = minUpdatePeriod;
|
this.minUpdatePeriod = minUpdatePeriod;
|
||||||
|
this.timeShiftBufferDepth = timeShiftBufferDepth;
|
||||||
|
this.utcTiming = utcTiming;
|
||||||
this.periods = Collections.unmodifiableList(periods);
|
this.periods = Collections.unmodifiableList(periods);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
@ -60,11 +58,7 @@ public final class MediaPresentationDescriptionFetcher extends
|
|||||||
@Override
|
@Override
|
||||||
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
||||||
String contentId, Uri baseUrl) throws IOException, ParserException {
|
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||||
try {
|
|
||||||
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
|
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
throw new ParserException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,13 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@ -48,6 +53,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
private static final Pattern DURATION =
|
private static final Pattern DURATION =
|
||||||
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
|
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;
|
private final XmlPullParserFactory xmlParserFactory;
|
||||||
|
|
||||||
public MediaPresentationDescriptionParser() {
|
public MediaPresentationDescriptionParser() {
|
||||||
@ -69,12 +79,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
|
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
|
||||||
* @return The parsed manifest.
|
* @return The parsed manifest.
|
||||||
* @throws IOException If a problem occurred reading from the stream.
|
* @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.
|
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
|
||||||
*/
|
*/
|
||||||
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
|
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
|
||||||
String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
|
String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||||
IOException, ParserException {
|
try {
|
||||||
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
||||||
xpp.setInput(inputStream, inputEncoding);
|
xpp.setInput(inputStream, inputEncoding);
|
||||||
int eventType = xpp.next();
|
int eventType = xpp.next();
|
||||||
@ -83,28 +92,44 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
"inputStream does not contain a valid media presentation description");
|
"inputStream does not contain a valid media presentation description");
|
||||||
}
|
}
|
||||||
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
|
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
throw new ParserException(e);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new ParserException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
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 durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
|
||||||
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
|
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
|
||||||
String typeString = xpp.getAttributeValue(null, "type");
|
String typeString = xpp.getAttributeValue(null, "type");
|
||||||
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
|
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
|
||||||
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
||||||
|
long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1;
|
||||||
|
UtcTimingElement utcTiming = null;
|
||||||
|
|
||||||
List<Period> periods = new ArrayList<Period>();
|
List<Period> periods = new ArrayList<Period>();
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
if (isStartTag(xpp, "BaseURL")) {
|
if (isStartTag(xpp, "BaseURL")) {
|
||||||
baseUrl = parseBaseUrl(xpp, baseUrl);
|
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||||
|
} else if (isStartTag(xpp, "UTCTiming")) {
|
||||||
|
utcTiming = parseUtcTiming(xpp);
|
||||||
} else if (isStartTag(xpp, "Period")) {
|
} else if (isStartTag(xpp, "Period")) {
|
||||||
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
|
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
|
||||||
}
|
}
|
||||||
} while (!isEndTag(xpp, "MPD"));
|
} while (!isEndTag(xpp, "MPD"));
|
||||||
|
|
||||||
return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
|
return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
|
||||||
periods);
|
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)
|
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);
|
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) {
|
private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
|
||||||
String value = xpp.getAttributeValue(null, name);
|
String value = xpp.getAttributeValue(null, name);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<UUID, byte[]> getPsshInfo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes data from a {@link NonBlockingInputStream}.
|
||||||
|
* <p>
|
||||||
|
* The read terminates if the end of the input stream is reached, if an attempt to read from the
|
||||||
|
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
|
||||||
|
* both the reason for termination and data that was parsed during the read.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which data should be read.
|
||||||
|
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
|
||||||
|
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
|
||||||
|
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
||||||
|
* @throws ParserException If an error occurs parsing the media data.
|
||||||
|
*/
|
||||||
|
public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeks to a position before or equal to the requested time.
|
||||||
|
*
|
||||||
|
* @param seekTimeUs The desired seek time in microseconds.
|
||||||
|
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
|
||||||
|
* fragment run, is equal to or greater than the time of the current sample, and if there
|
||||||
|
* does not exist a sync frame between these two times.
|
||||||
|
* @return True if the operation resulted in a change of state. False if it was a no-op.
|
||||||
|
*/
|
||||||
|
public boolean seekTo(long seekTimeUs, boolean allowNoop);
|
||||||
|
|
||||||
|
}
|
@ -18,11 +18,13 @@ package com.google.android.exoplayer.parser.mp4;
|
|||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
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.SegmentIndex;
|
||||||
import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom;
|
import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom;
|
||||||
import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom;
|
import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@ -47,7 +49,7 @@ import java.util.UUID;
|
|||||||
* <p>
|
* <p>
|
||||||
* This implementation only supports de-muxed (i.e. single track) streams.
|
* 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.
|
* 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;
|
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
|
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
|
||||||
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
|
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
|
||||||
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
|
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() {
|
public void setTrack(Track track) {
|
||||||
return segmentIndex;
|
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
|
||||||
}
|
this.track = track;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<UUID, byte[]> getPsshInfo() {
|
|
||||||
return psshData.isEmpty() ? null : psshData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -228,38 +195,27 @@ public final class FragmentedMp4Extractor {
|
|||||||
psshData.put(uuid, data);
|
psshData.put(uuid, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Returns the format of the samples contained within the media stream.
|
public Map<UUID, byte[]> getPsshInfo() {
|
||||||
*
|
return psshData.isEmpty() ? null : psshData;
|
||||||
* @return The sample media format, or null if a MOOV atom has yet to be parsed.
|
}
|
||||||
*/
|
|
||||||
|
@Override
|
||||||
|
public SegmentIndex getIndex() {
|
||||||
|
return segmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasRelativeIndexOffsets() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public MediaFormat getFormat() {
|
public MediaFormat getFormat() {
|
||||||
return track == null ? null : track.mediaFormat;
|
return track == null ? null : track.mediaFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* 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}.
|
|
||||||
* <p>
|
|
||||||
* The read terminates if the end of the input stream is reached, if an attempt to read from the
|
|
||||||
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
|
|
||||||
* both the reason for termination and data that was parsed during the read.
|
|
||||||
*
|
|
||||||
* @param inputStream The input stream from which data should be read.
|
|
||||||
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
|
|
||||||
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
|
|
||||||
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
|
||||||
* @throws ParserException If an error occurs parsing the media data.
|
|
||||||
*/
|
|
||||||
public int read(NonBlockingInputStream inputStream, SampleHolder out)
|
public int read(NonBlockingInputStream inputStream, SampleHolder out)
|
||||||
throws ParserException {
|
throws ParserException {
|
||||||
try {
|
try {
|
||||||
@ -286,15 +242,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* 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) {
|
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
||||||
pendingSeekTimeMs = (int) (seekTimeUs / 1000);
|
pendingSeekTimeMs = (int) (seekTimeUs / 1000);
|
||||||
if (allowNoop && fragmentRun != null
|
if (allowNoop && fragmentRun != null
|
||||||
@ -780,10 +728,10 @@ public final class FragmentedMp4Extractor {
|
|||||||
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
||||||
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
|
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
|
||||||
|
|
||||||
TrackEncryptionBox trackEncryptionBox =
|
|
||||||
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
|
|
||||||
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
||||||
if (saiz != null) {
|
if (saiz != null) {
|
||||||
|
TrackEncryptionBox trackEncryptionBox =
|
||||||
|
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
|
||||||
parseSaiz(trackEncryptionBox, saiz.data, out);
|
parseSaiz(trackEncryptionBox, saiz.data, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
|
||||||
*
|
|
||||||
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
|
|
||||||
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
|
||||||
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
|
|
||||||
*/
|
|
||||||
@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.
|
|
||||||
*
|
|
||||||
* <p>Replaces the previous {@link #format} only if video width/height have changed.
|
|
||||||
* {@link #format} is guaranteed to not be null after calling this method. In
|
|
||||||
* the event that it can't be built, an {@link 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.
|
|
||||||
*
|
|
||||||
* <p>{@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -17,76 +17,460 @@ package com.google.android.exoplayer.parser.webm;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
|
import com.google.android.exoplayer.parser.Extractor;
|
||||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
import com.google.android.exoplayer.parser.SegmentIndex;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
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.
|
||||||
*
|
*
|
||||||
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
|
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
|
||||||
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
||||||
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
|
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
|
||||||
*/
|
*/
|
||||||
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<UUID, byte[]> 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.
|
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
|
||||||
*/
|
|
||||||
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}.
|
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read
|
* <p>Replaces the previous {@link #format} only if video width/height have changed.
|
||||||
* @param sampleHolder A {@link SampleHolder} into which the sample should be read
|
* {@link #format} is guaranteed to not be null after calling this method. In
|
||||||
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
* 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
|
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
|
||||||
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
|
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
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.
|
* Passes events through to {@link WebmExtractor} as
|
||||||
*
|
* callbacks from {@link EbmlReader} are received.
|
||||||
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
|
|
||||||
* prepared
|
|
||||||
*/
|
*/
|
||||||
public SegmentIndex getIndex();
|
private final class InnerEbmlEventHandler implements EbmlEventHandler {
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Returns the format of the samples contained within the media stream.
|
public int getElementType(int id) {
|
||||||
*
|
return WebmExtractor.this.getElementType(id);
|
||||||
* @return The sample media format, or null if the extracted is not yet prepared
|
}
|
||||||
*/
|
|
||||||
public MediaFormat getFormat();
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.FormatEvaluator.Evaluation;
|
||||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||||
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
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.FragmentedMp4Extractor;
|
||||||
import com.google.android.exoplayer.parser.mp4.Track;
|
import com.google.android.exoplayer.parser.mp4.Track;
|
||||||
import com.google.android.exoplayer.parser.mp4.TrackEncryptionBox;
|
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.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
@ -227,7 +228,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
|
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
|
||||||
FragmentedMp4Extractor extractor, DataSource dataSource, int chunkIndex,
|
Extractor extractor, DataSource dataSource, int chunkIndex,
|
||||||
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
|
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
|
||||||
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
|
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
|
||||||
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
|
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;
|
||||||
|
@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
@ -59,11 +57,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth
|
|||||||
@Override
|
@Override
|
||||||
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
|
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
|
||||||
String contentId, Uri baseUrl) throws IOException, ParserException {
|
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||||
try {
|
|
||||||
return parser.parse(stream, inputEncoding);
|
return parser.parse(stream, inputEncoding);
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
throw new ParserException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
package com.google.android.exoplayer.smoothstreaming;
|
package com.google.android.exoplayer.smoothstreaming;
|
||||||
|
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
|
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
@ -60,15 +60,18 @@ public class SmoothStreamingManifestParser {
|
|||||||
* @param inputEncoding The encoding of the input.
|
* @param inputEncoding The encoding of the input.
|
||||||
* @return The parsed manifest.
|
* @return The parsed manifest.
|
||||||
* @throws IOException If a problem occurred reading from the stream.
|
* @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 smooth streaming manifest.
|
* @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest.
|
||||||
*/
|
*/
|
||||||
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
|
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
|
||||||
XmlPullParserException, IOException, ParserException {
|
IOException, ParserException {
|
||||||
|
try {
|
||||||
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
||||||
xmlParser.setInput(inputStream, inputEncoding);
|
xmlParser.setInput(inputStream, inputEncoding);
|
||||||
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
|
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
|
||||||
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
|
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
throw new ParserException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,6 +105,7 @@ public class SmoothStreamingManifestParser {
|
|||||||
ParserException {
|
ParserException {
|
||||||
String tagName;
|
String tagName;
|
||||||
boolean foundStartTag = false;
|
boolean foundStartTag = false;
|
||||||
|
int skippingElementDepth = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
int eventType = xmlParser.getEventType();
|
int eventType = xmlParser.getEventType();
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
@ -111,26 +115,37 @@ public class SmoothStreamingManifestParser {
|
|||||||
foundStartTag = true;
|
foundStartTag = true;
|
||||||
parseStartTag(xmlParser);
|
parseStartTag(xmlParser);
|
||||||
} else if (foundStartTag) {
|
} else if (foundStartTag) {
|
||||||
if (handleChildInline(tagName)) {
|
if (skippingElementDepth > 0) {
|
||||||
|
skippingElementDepth++;
|
||||||
|
} else if (handleChildInline(tagName)) {
|
||||||
parseStartTag(xmlParser);
|
parseStartTag(xmlParser);
|
||||||
} else {
|
} else {
|
||||||
addChild(newChildParser(this, tagName).parse(xmlParser));
|
ElementParser childElementParser = newChildParser(this, tagName);
|
||||||
|
if (childElementParser == null) {
|
||||||
|
skippingElementDepth = 1;
|
||||||
|
} else {
|
||||||
|
addChild(childElementParser.parse(xmlParser));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case XmlPullParser.TEXT:
|
case XmlPullParser.TEXT:
|
||||||
if (foundStartTag) {
|
if (foundStartTag && skippingElementDepth == 0) {
|
||||||
parseText(xmlParser);
|
parseText(xmlParser);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case XmlPullParser.END_TAG:
|
case XmlPullParser.END_TAG:
|
||||||
if (foundStartTag) {
|
if (foundStartTag) {
|
||||||
|
if (skippingElementDepth > 0) {
|
||||||
|
skippingElementDepth--;
|
||||||
|
} else {
|
||||||
tagName = xmlParser.getName();
|
tagName = xmlParser.getName();
|
||||||
parseEndTag(xmlParser);
|
parseEndTag(xmlParser);
|
||||||
if (!handleChildInline(tagName)) {
|
if (!handleChildInline(tagName)) {
|
||||||
return build();
|
return build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case XmlPullParser.END_DOCUMENT:
|
case XmlPullParser.END_DOCUMENT:
|
||||||
return null;
|
return null;
|
||||||
@ -357,6 +372,7 @@ public class SmoothStreamingManifestParser {
|
|||||||
|
|
||||||
public static final String KEY_SYSTEM_ID = "SystemID";
|
public static final String KEY_SYSTEM_ID = "SystemID";
|
||||||
|
|
||||||
|
private boolean inProtectionHeader;
|
||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
private byte[] initData;
|
private byte[] initData;
|
||||||
|
|
||||||
@ -371,17 +387,26 @@ public class SmoothStreamingManifestParser {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void parseStartTag(XmlPullParser parser) {
|
public void parseStartTag(XmlPullParser parser) {
|
||||||
if (!TAG_PROTECTION_HEADER.equals(parser.getName())) {
|
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
|
||||||
return;
|
inProtectionHeader = true;
|
||||||
}
|
|
||||||
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
|
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
|
||||||
uuid = UUID.fromString(uuidString);
|
uuid = UUID.fromString(uuidString);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void parseText(XmlPullParser parser) {
|
public void parseText(XmlPullParser parser) {
|
||||||
|
if (inProtectionHeader) {
|
||||||
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
|
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parseEndTag(XmlPullParser parser) {
|
||||||
|
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
|
||||||
|
inProtectionHeader = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object build() {
|
public Object build() {
|
||||||
@ -579,9 +604,11 @@ public class SmoothStreamingManifestParser {
|
|||||||
if (type == StreamElement.TYPE_VIDEO) {
|
if (type == StreamElement.TYPE_VIDEO) {
|
||||||
maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
|
maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
|
||||||
maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
|
maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
|
||||||
|
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
|
||||||
} else {
|
} else {
|
||||||
maxHeight = -1;
|
maxHeight = -1;
|
||||||
maxWidth = -1;
|
maxWidth = -1;
|
||||||
|
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == StreamElement.TYPE_AUDIO) {
|
if (type == StreamElement.TYPE_AUDIO) {
|
||||||
@ -590,14 +617,12 @@ public class SmoothStreamingManifestParser {
|
|||||||
bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
|
bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
|
||||||
packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
|
packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
|
||||||
audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
|
audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
|
||||||
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
|
|
||||||
} else {
|
} else {
|
||||||
samplingRate = -1;
|
samplingRate = -1;
|
||||||
channels = -1;
|
channels = -1;
|
||||||
bitPerSample = -1;
|
bitPerSample = -1;
|
||||||
packetSize = -1;
|
packetSize = -1;
|
||||||
audioTag = -1;
|
audioTag = -1;
|
||||||
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
|
value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
package com.google.android.exoplayer.text;
|
package com.google.android.exoplayer.text;
|
||||||
|
|
||||||
import com.google.android.exoplayer.ExoPlaybackException;
|
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.SampleHolder;
|
||||||
import com.google.android.exoplayer.SampleSource;
|
import com.google.android.exoplayer.SampleSource;
|
||||||
import com.google.android.exoplayer.TrackRenderer;
|
import com.google.android.exoplayer.TrackRenderer;
|
||||||
@ -64,7 +64,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
private final TextRenderer textRenderer;
|
private final TextRenderer textRenderer;
|
||||||
private final SampleSource source;
|
private final SampleSource source;
|
||||||
private final SampleHolder sampleHolder;
|
private final SampleHolder sampleHolder;
|
||||||
private final FormatHolder formatHolder;
|
private final MediaFormatHolder formatHolder;
|
||||||
private final SubtitleParser subtitleParser;
|
private final SubtitleParser subtitleParser;
|
||||||
|
|
||||||
private int trackIndex;
|
private int trackIndex;
|
||||||
@ -93,7 +93,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
this.textRenderer = Assertions.checkNotNull(textRenderer);
|
||||||
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
|
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
|
||||||
this);
|
this);
|
||||||
formatHolder = new FormatHolder();
|
formatHolder = new MediaFormatHolder();
|
||||||
sampleHolder = new SampleHolder(true);
|
sampleHolder = new SampleHolder(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +41,10 @@ public final class CacheDataSource implements DataSource {
|
|||||||
public interface EventListener {
|
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 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);
|
void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
|
||||||
|
|
||||||
|
@ -13,9 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.parser.mp4;
|
package com.google.android.exoplayer.util;
|
||||||
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
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
|
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 static final int SPS_NAL_UNIT_TYPE = 7;
|
||||||
|
|
||||||
private CodecSpecificDataUtil() {}
|
private CodecSpecificDataUtil() {}
|
||||||
@ -42,7 +44,7 @@ public final class CodecSpecificDataUtil {
|
|||||||
/**
|
/**
|
||||||
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
* 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.
|
* @return A pair consisting of the sample rate in Hz and the channel count.
|
||||||
*/
|
*/
|
||||||
public static Pair<Integer, Integer> parseAudioSpecificConfig(byte[] audioSpecificConfig) {
|
public static Pair<Integer, Integer> parseAudioSpecificConfig(byte[] audioSpecificConfig) {
|
||||||
@ -56,11 +58,27 @@ public final class CodecSpecificDataUtil {
|
|||||||
return Pair.create(sampleRate, channelCount);
|
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
|
* 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 sampleRate The sample rate in Hz.
|
||||||
* @param numChannels The number of channels
|
* @param numChannels The number of channels.
|
||||||
* @return The AudioSpecificConfig.
|
* @return The AudioSpecificConfig.
|
||||||
*/
|
*/
|
||||||
public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
|
public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
|
||||||
@ -70,10 +88,16 @@ public final class CodecSpecificDataUtil {
|
|||||||
sampleRateIndex = i;
|
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
|
// The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1
|
||||||
byte[] csd = new byte[2];
|
byte[] csd = new byte[2];
|
||||||
csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1));
|
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;
|
return csd;
|
||||||
}
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user