mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
commit
4228f2cfa3
10
README.md
10
README.md
@ -26,6 +26,16 @@ get started.
|
|||||||
[Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html
|
[Class reference]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/package-summary.html
|
||||||
|
|
||||||
|
|
||||||
|
## Project branches ##
|
||||||
|
|
||||||
|
* The [master][] branch holds the most recent minor release.
|
||||||
|
* Most development work happens on the [dev][] branch.
|
||||||
|
* Additional development branches may be established for major features.
|
||||||
|
|
||||||
|
[master]: https://github.com/google/ExoPlayer/tree/master
|
||||||
|
[dev]: https://github.com/google/ExoPlayer/tree/dev
|
||||||
|
|
||||||
|
|
||||||
## Using Eclipse ##
|
## Using Eclipse ##
|
||||||
|
|
||||||
The repository includes Eclipse projects for both the ExoPlayer library and its
|
The repository includes Eclipse projects for both the ExoPlayer library and its
|
||||||
|
@ -18,7 +18,7 @@ android {
|
|||||||
buildToolsVersion "19.1"
|
buildToolsVersion "19.1"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 9
|
minSdkVersion 16
|
||||||
targetSdkVersion 19
|
targetSdkVersion 19
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity {
|
|||||||
sampleAdapter.addAll((Object[]) Samples.SIMPLE);
|
sampleAdapter.addAll((Object[]) Samples.SIMPLE);
|
||||||
sampleAdapter.add(new Header("YouTube DASH"));
|
sampleAdapter.add(new Header("YouTube DASH"));
|
||||||
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4);
|
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4);
|
||||||
sampleAdapter.add(new Header("Widevine DASH GTS"));
|
sampleAdapter.add(new Header("Widevine GTS DASH"));
|
||||||
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS);
|
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS);
|
||||||
sampleAdapter.add(new Header("SmoothStreaming"));
|
sampleAdapter.add(new Header("SmoothStreaming"));
|
||||||
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
|
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);
|
||||||
|
@ -91,7 +91,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
|
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
|
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
|
||||||
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
||||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||||
@ -110,13 +110,13 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs) {
|
public void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
|
||||||
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " +
|
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + formatId + ", " +
|
||||||
Integer.toString(trigger) + "]");
|
Integer.toString(trigger) + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs) {
|
public void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs) {
|
||||||
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " +
|
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + formatId + ", " +
|
||||||
Integer.toString(trigger) + "]");
|
Integer.toString(trigger) + "]");
|
||||||
}
|
}
|
||||||
|
@ -160,8 +160,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
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)) {
|
||||||
@ -192,8 +191,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
|||||||
audioChunkSource = null;
|
audioChunkSource = null;
|
||||||
audioRenderer = null;
|
audioRenderer = null;
|
||||||
} else {
|
} else {
|
||||||
DataSource audioDataSource = new HttpDataSource(userAgent,
|
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
|
|
||||||
audioTrackNames = new String[audioRepresentationsList.size()];
|
audioTrackNames = new String[audioRepresentationsList.size()];
|
||||||
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
|
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
|
||||||
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||||
|
@ -118,11 +118,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
* A listener for debugging information.
|
* A listener for debugging information.
|
||||||
*/
|
*/
|
||||||
public interface InfoListener {
|
public interface InfoListener {
|
||||||
void onVideoFormatEnabled(int formatId, int trigger, int mediaTimeMs);
|
void onVideoFormatEnabled(String formatId, int trigger, int mediaTimeMs);
|
||||||
void onAudioFormatEnabled(int formatId, int trigger, int mediaTimeMs);
|
void onAudioFormatEnabled(String formatId, int trigger, int mediaTimeMs);
|
||||||
void onDroppedFrames(int count, long elapsed);
|
void onDroppedFrames(int count, long elapsed);
|
||||||
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
|
void onBandwidthSample(int elapsedMs, long bytes, long bandwidthEstimate);
|
||||||
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
|
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
||||||
void onLoadCompleted(int sourceId);
|
void onLoadCompleted(int sourceId);
|
||||||
}
|
}
|
||||||
@ -398,7 +398,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs) {
|
public void onDownstreamFormatChanged(int sourceId, String formatId, int trigger,
|
||||||
|
int mediaTimeMs) {
|
||||||
if (infoListener == null) {
|
if (infoListener == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -469,7 +470,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
|
public void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
|
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes) {
|
||||||
if (infoListener != null) {
|
if (infoListener != null) {
|
||||||
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
|
infoListener.onLoadStarted(sourceId, formatId, trigger, isInitialization, mediaStartTimeMs,
|
||||||
|
@ -150,8 +150,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
bandwidthMeter);
|
|
||||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
||||||
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
||||||
new AdaptiveEvaluator(bandwidthMeter));
|
new AdaptiveEvaluator(bandwidthMeter));
|
||||||
@ -173,8 +172,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
} else {
|
} else {
|
||||||
audioTrackNames = new String[audioStreamElementCount];
|
audioTrackNames = new String[audioStreamElementCount];
|
||||||
ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
|
ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
|
||||||
DataSource audioDataSource = new HttpDataSource(userAgent,
|
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
|
|
||||||
FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
|
FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||||
audioStreamElementCount = 0;
|
audioStreamElementCount = 0;
|
||||||
for (int i = 0; i < manifest.streamElements.length; i++) {
|
for (int i = 0; i < manifest.streamElements.length; i++) {
|
||||||
@ -204,8 +202,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
} else {
|
} else {
|
||||||
textTrackNames = new String[textStreamElementCount];
|
textTrackNames = new String[textStreamElementCount];
|
||||||
ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
|
ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
|
||||||
DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
bandwidthMeter);
|
|
||||||
FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
|
FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||||
textStreamElementCount = 0;
|
textStreamElementCount = 0;
|
||||||
for (int i = 0; i < manifest.streamElements.length; i++) {
|
for (int i = 0; i < manifest.streamElements.length; i++) {
|
||||||
|
@ -115,8 +115,7 @@ import java.util.ArrayList;
|
|||||||
videoRepresentationsList.toArray(videoRepresentations);
|
videoRepresentationsList.toArray(videoRepresentations);
|
||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
bandwidthMeter);
|
|
||||||
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
|
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
|
||||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
@ -125,8 +124,7 @@ import java.util.ArrayList;
|
|||||||
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
|
||||||
|
|
||||||
// Build the audio renderer.
|
// Build the audio renderer.
|
||||||
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
bandwidthMeter);
|
|
||||||
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
|
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
|
||||||
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
|
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
|
||||||
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||||
|
@ -115,8 +115,7 @@ import java.util.ArrayList;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
bandwidthMeter);
|
|
||||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
||||||
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
||||||
new AdaptiveEvaluator(bandwidthMeter));
|
new AdaptiveEvaluator(bandwidthMeter));
|
||||||
@ -126,8 +125,7 @@ import java.util.ArrayList;
|
|||||||
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
|
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
|
||||||
|
|
||||||
// Build the audio renderer.
|
// Build the audio renderer.
|
||||||
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
|
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
bandwidthMeter);
|
|
||||||
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
||||||
audioStreamElementIndex, new int[] {0}, audioDataSource,
|
audioStreamElementIndex, new int[] {0}, audioDataSource,
|
||||||
new FormatEvaluator.FixedEvaluator());
|
new FormatEvaluator.FixedEvaluator());
|
||||||
|
@ -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.10";
|
public static final String VERSION = "1.0.11";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library, expressed as an integer.
|
* The version of the library, expressed as an integer.
|
||||||
|
@ -95,6 +95,10 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
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;
|
||||||
|
|
||||||
|
private static final int START_NOT_SET = 0;
|
||||||
|
private static final int START_IN_SYNC = 1;
|
||||||
|
private static final int START_NEED_SYNC = 2;
|
||||||
|
|
||||||
private final EventListener eventListener;
|
private final EventListener eventListener;
|
||||||
private final ConditionVariable audioTrackReleasingConditionVariable;
|
private final ConditionVariable audioTrackReleasingConditionVariable;
|
||||||
private final AudioTimestampCompat audioTimestampCompat;
|
private final AudioTimestampCompat audioTimestampCompat;
|
||||||
@ -119,7 +123,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
private Method audioTrackGetLatencyMethod;
|
private Method audioTrackGetLatencyMethod;
|
||||||
private int audioSessionId;
|
private int audioSessionId;
|
||||||
private long submittedBytes;
|
private long submittedBytes;
|
||||||
private boolean audioTrackStartMediaTimeSet;
|
private int audioTrackStartMediaTimeState;
|
||||||
private long audioTrackStartMediaTimeUs;
|
private long audioTrackStartMediaTimeUs;
|
||||||
private long audioTrackResumeSystemTimeUs;
|
private long audioTrackResumeSystemTimeUs;
|
||||||
private long lastReportedCurrentPositionUs;
|
private long lastReportedCurrentPositionUs;
|
||||||
@ -363,7 +367,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
lastRawPlaybackHeadPosition = 0;
|
lastRawPlaybackHeadPosition = 0;
|
||||||
rawPlaybackHeadWrapCount = 0;
|
rawPlaybackHeadWrapCount = 0;
|
||||||
audioTrackStartMediaTimeUs = 0;
|
audioTrackStartMediaTimeUs = 0;
|
||||||
audioTrackStartMediaTimeSet = false;
|
audioTrackStartMediaTimeState = START_NOT_SET;
|
||||||
resetSyncParams();
|
resetSyncParams();
|
||||||
int playState = audioTrack.getPlayState();
|
int playState = audioTrack.getPlayState();
|
||||||
if (playState == AudioTrack.PLAYSTATE_PLAYING) {
|
if (playState == AudioTrack.PLAYSTATE_PLAYING) {
|
||||||
@ -429,7 +433,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
protected long getCurrentPositionUs() {
|
protected long getCurrentPositionUs() {
|
||||||
long systemClockUs = System.nanoTime() / 1000;
|
long systemClockUs = System.nanoTime() / 1000;
|
||||||
long currentPositionUs;
|
long currentPositionUs;
|
||||||
if (audioTrack == null || !audioTrackStartMediaTimeSet) {
|
if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET) {
|
||||||
// The AudioTrack hasn't started.
|
// The AudioTrack hasn't started.
|
||||||
currentPositionUs = super.getCurrentPositionUs();
|
currentPositionUs = super.getCurrentPositionUs();
|
||||||
} else if (audioTimestampSet) {
|
} else if (audioTimestampSet) {
|
||||||
@ -461,7 +465,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeSampleSyncParams() {
|
private void maybeSampleSyncParams() {
|
||||||
if (audioTrack == null || !audioTrackStartMediaTimeSet || getState() != STATE_STARTED) {
|
if (audioTrack == null || audioTrackStartMediaTimeState == START_NOT_SET
|
||||||
|
|| getState() != STATE_STARTED) {
|
||||||
// The AudioTrack isn't playing.
|
// The AudioTrack isn't playing.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -549,25 +554,40 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException {
|
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
if (shouldSkip) {
|
||||||
|
codec.releaseOutputBuffer(bufferIndex, false);
|
||||||
|
codecCounters.skippedOutputBufferCount++;
|
||||||
|
if (audioTrackStartMediaTimeState == START_IN_SYNC) {
|
||||||
|
// Skipping the sample will push track time out of sync. We'll need to sync again.
|
||||||
|
audioTrackStartMediaTimeState = START_NEED_SYNC;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (temporaryBufferSize == 0) {
|
if (temporaryBufferSize == 0) {
|
||||||
// This is the first time we've seen this {@code buffer}.
|
// This is the first time we've seen this {@code buffer}.
|
||||||
|
|
||||||
// Note: presentationTimeUs corresponds to the end of the sample, not the start.
|
// Note: presentationTimeUs corresponds to the end of the sample, not the start.
|
||||||
long bufferStartTime = bufferInfo.presentationTimeUs -
|
long bufferStartTime = bufferInfo.presentationTimeUs -
|
||||||
framesToDurationUs(bufferInfo.size / frameSize);
|
framesToDurationUs(bufferInfo.size / frameSize);
|
||||||
if (!audioTrackStartMediaTimeSet) {
|
if (audioTrackStartMediaTimeState == START_NOT_SET) {
|
||||||
audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
|
audioTrackStartMediaTimeUs = Math.max(0, bufferStartTime);
|
||||||
audioTrackStartMediaTimeSet = true;
|
audioTrackStartMediaTimeState = START_IN_SYNC;
|
||||||
} else {
|
} else {
|
||||||
// Sanity check that bufferStartTime is consistent with the expected value.
|
// Sanity check that bufferStartTime is consistent with the expected value.
|
||||||
long expectedBufferStartTime = audioTrackStartMediaTimeUs +
|
long expectedBufferStartTime = audioTrackStartMediaTimeUs +
|
||||||
framesToDurationUs(submittedBytes / frameSize);
|
framesToDurationUs(submittedBytes / frameSize);
|
||||||
if (Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
|
if (audioTrackStartMediaTimeState == START_IN_SYNC
|
||||||
|
&& Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
|
||||||
Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " +
|
Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " +
|
||||||
bufferStartTime + "]");
|
bufferStartTime + "]");
|
||||||
// Adjust audioTrackStartMediaTimeUs to compensate for the discontinuity. Also reset
|
audioTrackStartMediaTimeState = START_NEED_SYNC;
|
||||||
// lastReportedCurrentPositionUs to allow time to jump backwards if it really wants to.
|
}
|
||||||
|
if (audioTrackStartMediaTimeState == START_NEED_SYNC) {
|
||||||
|
// Adjust audioTrackStartMediaTimeUs to be consistent with the current buffer's start
|
||||||
|
// time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to
|
||||||
|
// allow time to jump backwards if it really wants to.
|
||||||
audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
|
audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
|
||||||
lastReportedCurrentPositionUs = 0;
|
lastReportedCurrentPositionUs = 0;
|
||||||
}
|
}
|
||||||
|
@ -391,7 +391,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
|
while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) {
|
||||||
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
||||||
if (result == SampleSource.SAMPLE_READ) {
|
if (result == SampleSource.SAMPLE_READ) {
|
||||||
currentPositionUs = sampleHolder.timeUs;
|
if (!sampleHolder.decodeOnly) {
|
||||||
|
currentPositionUs = sampleHolder.timeUs;
|
||||||
|
}
|
||||||
codecCounters.discardedSamplesCount++;
|
codecCounters.discardedSamplesCount++;
|
||||||
} else if (result == SampleSource.FORMAT_READ) {
|
} else if (result == SampleSource.FORMAT_READ) {
|
||||||
onInputFormatChanged(formatHolder);
|
onInputFormatChanged(formatHolder);
|
||||||
@ -677,15 +679,15 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs)) {
|
boolean decodeOnly = decodeOnlyPresentationTimestamps.contains(
|
||||||
codec.releaseOutputBuffer(outputIndex, false);
|
outputBufferInfo.presentationTimeUs);
|
||||||
outputIndex = -1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
|
if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo,
|
||||||
outputIndex)) {
|
outputIndex, decodeOnly)) {
|
||||||
currentPositionUs = outputBufferInfo.presentationTimeUs;
|
if (decodeOnly) {
|
||||||
|
decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs);
|
||||||
|
} else {
|
||||||
|
currentPositionUs = outputBufferInfo.presentationTimeUs;
|
||||||
|
}
|
||||||
outputIndex = -1;
|
outputIndex = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -701,7 +703,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|||||||
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
||||||
*/
|
*/
|
||||||
protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex) throws ExoPlaybackException;
|
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
|
||||||
|
throws ExoPlaybackException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the secure variant of a given decoder.
|
* Returns the name of the secure variant of a given decoder.
|
||||||
|
@ -29,7 +29,7 @@ import android.view.Surface;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes and renders video using {@MediaCodec}.
|
* Decodes and renders video using {@link MediaCodec}.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
||||||
@ -338,7 +338,12 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer,
|
||||||
MediaCodec.BufferInfo bufferInfo, int bufferIndex) {
|
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
|
||||||
|
if (shouldSkip) {
|
||||||
|
skipOutputBuffer(codec, bufferIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
long earlyUs = bufferInfo.presentationTimeUs - timeUs;
|
long earlyUs = bufferInfo.presentationTimeUs - timeUs;
|
||||||
if (earlyUs < -30000) {
|
if (earlyUs < -30000) {
|
||||||
// We're more than 30ms late rendering the frame.
|
// We're more than 30ms late rendering the frame.
|
||||||
@ -371,6 +376,13 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
|
||||||
|
TraceUtil.beginSection("skipVideoBuffer");
|
||||||
|
codec.releaseOutputBuffer(bufferIndex, false);
|
||||||
|
TraceUtil.endSection();
|
||||||
|
codecCounters.skippedOutputBufferCount++;
|
||||||
|
}
|
||||||
|
|
||||||
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
|
private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
|
||||||
TraceUtil.beginSection("dropVideoBuffer");
|
TraceUtil.beginSection("dropVideoBuffer");
|
||||||
codec.releaseOutputBuffer(bufferIndex, false);
|
codec.releaseOutputBuffer(bufferIndex, false);
|
||||||
|
@ -54,8 +54,8 @@ public interface SampleSource {
|
|||||||
* Prepares the source.
|
* Prepares the source.
|
||||||
* <p>
|
* <p>
|
||||||
* Preparation may require reading from the data source (e.g. to determine the available tracks
|
* Preparation may require reading from the data source (e.g. to determine the available tracks
|
||||||
* and formats). If insufficient data is available then the call will return rather than block.
|
* and formats). If insufficient data is available then the call will return {@code false} rather
|
||||||
* The method can be called repeatedly until the return value indicates success.
|
* than block. The method can be called repeatedly until the return value indicates success.
|
||||||
*
|
*
|
||||||
* @return True if the source was prepared successfully, false otherwise.
|
* @return True if the source was prepared successfully, false otherwise.
|
||||||
* @throws IOException If an error occurred preparing the source.
|
* @throws IOException If an error occurred preparing the source.
|
||||||
|
@ -59,7 +59,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
* load is for initialization data.
|
* load is for initialization data.
|
||||||
* @param totalBytes The length of the data being loaded in bytes.
|
* @param totalBytes The length of the data being loaded in bytes.
|
||||||
*/
|
*/
|
||||||
void onLoadStarted(int sourceId, int formatId, int trigger, boolean isInitialization,
|
void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
|
||||||
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
int mediaStartTimeMs, int mediaEndTimeMs, long totalBytes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,7 +126,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
* {@link ChunkSource}.
|
* {@link ChunkSource}.
|
||||||
* @param mediaTimeMs The media time at which the change occurred.
|
* @param mediaTimeMs The media time at which the change occurred.
|
||||||
*/
|
*/
|
||||||
void onDownstreamFormatChanged(int sourceId, int formatId, int trigger, int mediaTimeMs);
|
void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, int mediaTimeMs);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +160,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
private int currentLoadableExceptionCount;
|
private int currentLoadableExceptionCount;
|
||||||
private long currentLoadableExceptionTimestamp;
|
private long currentLoadableExceptionTimestamp;
|
||||||
|
|
||||||
|
private MediaFormat downstreamMediaFormat;
|
||||||
private volatile Format downstreamFormat;
|
private volatile Format downstreamFormat;
|
||||||
|
|
||||||
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
|
||||||
@ -221,6 +222,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
chunkSource.enable();
|
chunkSource.enable();
|
||||||
loadControl.register(this, bufferSizeContribution);
|
loadControl.register(this, bufferSizeContribution);
|
||||||
downstreamFormat = null;
|
downstreamFormat = null;
|
||||||
|
downstreamMediaFormat = null;
|
||||||
downstreamPositionUs = timeUs;
|
downstreamPositionUs = timeUs;
|
||||||
lastSeekPositionUs = timeUs;
|
lastSeekPositionUs = timeUs;
|
||||||
restartFrom(timeUs);
|
restartFrom(timeUs);
|
||||||
@ -288,21 +290,30 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
|
return readData(track, playbackPositionUs, formatHolder, sampleHolder, false);
|
||||||
} else if (mediaChunk.isLastChunk()) {
|
} else if (mediaChunk.isLastChunk()) {
|
||||||
return END_OF_STREAM;
|
return END_OF_STREAM;
|
||||||
} else {
|
|
||||||
IOException chunkSourceException = chunkSource.getError();
|
|
||||||
if (chunkSourceException != null) {
|
|
||||||
throw chunkSourceException;
|
|
||||||
}
|
|
||||||
return NOTHING_READ;
|
|
||||||
}
|
}
|
||||||
} else if (downstreamFormat == null || downstreamFormat.id != mediaChunk.format.id) {
|
IOException chunkSourceException = chunkSource.getError();
|
||||||
|
if (chunkSourceException != null) {
|
||||||
|
throw chunkSourceException;
|
||||||
|
}
|
||||||
|
return NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
|
||||||
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
|
notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
|
||||||
mediaChunk.startTimeUs);
|
mediaChunk.startTimeUs);
|
||||||
MediaFormat format = mediaChunk.getMediaFormat();
|
|
||||||
chunkSource.getMaxVideoDimensions(format);
|
|
||||||
formatHolder.format = format;
|
|
||||||
formatHolder.drmInitData = mediaChunk.getPsshInfo();
|
|
||||||
downstreamFormat = mediaChunk.format;
|
downstreamFormat = mediaChunk.format;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mediaChunk.prepare()) {
|
||||||
|
return NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
|
||||||
|
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) {
|
||||||
|
chunkSource.getMaxVideoDimensions(mediaFormat);
|
||||||
|
formatHolder.format = mediaFormat;
|
||||||
|
formatHolder.drmInitData = mediaChunk.getPsshInfo();
|
||||||
|
downstreamMediaFormat = mediaFormat;
|
||||||
return FORMAT_READ;
|
return FORMAT_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,6 +441,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
currentLoadableExceptionCount++;
|
currentLoadableExceptionCount++;
|
||||||
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
|
||||||
notifyUpstreamError(e);
|
notifyUpstreamError(e);
|
||||||
|
chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
|
||||||
updateLoadControl();
|
updateLoadControl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,7 +665,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
return (int) (timeUs / 1000);
|
return (int) (timeUs / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyLoadStarted(final int formatId, final int trigger,
|
private void notifyLoadStarted(final String formatId, final int trigger,
|
||||||
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
|
final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
|
||||||
final long totalBytes) {
|
final long totalBytes) {
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
@ -724,7 +736,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyDownstreamFormatChanged(final int formatId, final int trigger,
|
private void notifyDownstreamFormatChanged(final String formatId, final int trigger,
|
||||||
final long mediaTimeUs) {
|
final long mediaTimeUs) {
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
|
@ -58,7 +58,7 @@ public interface ChunkSource {
|
|||||||
*
|
*
|
||||||
* @param queue A representation of the currently buffered {@link MediaChunk}s.
|
* @param queue A representation of the currently buffered {@link MediaChunk}s.
|
||||||
*/
|
*/
|
||||||
void disable(List<MediaChunk> queue);
|
void disable(List<? extends MediaChunk> queue);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates to the source that it should still be checking for updates to the stream.
|
* Indicates to the source that it should still be checking for updates to the stream.
|
||||||
@ -100,4 +100,13 @@ public interface ChunkSource {
|
|||||||
*/
|
*/
|
||||||
IOException getError();
|
IOException getError();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
|
||||||
|
* this source.
|
||||||
|
*
|
||||||
|
* @param chunk The chunk whose load encountered the error.
|
||||||
|
* @param e The error.
|
||||||
|
*/
|
||||||
|
void onChunkLoadError(Chunk chunk, Exception e);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.chunk;
|
package com.google.android.exoplayer.chunk;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A format definition for streams.
|
* A format definition for streams.
|
||||||
*/
|
*/
|
||||||
public final class Format {
|
public class Format {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts {@link Format} objects in order of decreasing bandwidth.
|
* Sorts {@link Format} objects in order of decreasing bandwidth.
|
||||||
@ -29,7 +31,7 @@ public final class Format {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compare(Format a, Format b) {
|
public int compare(Format a, Format b) {
|
||||||
return b.bandwidth - a.bandwidth;
|
return b.bitrate - a.bitrate;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -37,7 +39,7 @@ public final class Format {
|
|||||||
/**
|
/**
|
||||||
* An identifier for the format.
|
* An identifier for the format.
|
||||||
*/
|
*/
|
||||||
public final int id;
|
public final String id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mime type of the format.
|
* The mime type of the format.
|
||||||
@ -65,8 +67,16 @@ public final class Format {
|
|||||||
public final int audioSamplingRate;
|
public final int audioSamplingRate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The average bandwidth in bytes per second.
|
* The average bandwidth in bits per second.
|
||||||
*/
|
*/
|
||||||
|
public final int bitrate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The average bandwidth in bytes per second.
|
||||||
|
*
|
||||||
|
* @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public final int bandwidth;
|
public final int bandwidth;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,17 +86,38 @@ public final class Format {
|
|||||||
* @param height The height of the video in pixels, or -1 for non-video formats.
|
* @param height The height of the video in pixels, or -1 for non-video formats.
|
||||||
* @param numChannels The number of audio channels, or -1 for non-audio formats.
|
* @param numChannels The number of audio channels, or -1 for non-audio formats.
|
||||||
* @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
|
* @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
|
||||||
* @param bandwidth The average bandwidth of the format in bytes per second.
|
* @param bitrate The average bandwidth of the format in bits per second.
|
||||||
*/
|
*/
|
||||||
public Format(int id, String mimeType, int width, int height, int numChannels,
|
public Format(String id, String mimeType, int width, int height, int numChannels,
|
||||||
int audioSamplingRate, int bandwidth) {
|
int audioSamplingRate, int bitrate) {
|
||||||
this.id = id;
|
this.id = Assertions.checkNotNull(id);
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.numChannels = numChannels;
|
this.numChannels = numChannels;
|
||||||
this.audioSamplingRate = audioSamplingRate;
|
this.audioSamplingRate = audioSamplingRate;
|
||||||
this.bandwidth = bandwidth;
|
this.bitrate = bitrate;
|
||||||
|
this.bandwidth = bitrate / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return id.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements equality based on {@link #id} only.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Format other = (Format) obj;
|
||||||
|
return other.id.equals(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ public interface FormatEvaluator {
|
|||||||
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
|
public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
|
||||||
Format[] formats, Evaluation evaluation) {
|
Format[] formats, Evaluation evaluation) {
|
||||||
Format newFormat = formats[random.nextInt(formats.length)];
|
Format newFormat = formats[random.nextInt(formats.length)];
|
||||||
if (evaluation.format != null && evaluation.format.id != newFormat.id) {
|
if (evaluation.format != null && !evaluation.format.id.equals(newFormat.id)) {
|
||||||
evaluation.trigger = TRIGGER_ADAPTIVE;
|
evaluation.trigger = TRIGGER_ADAPTIVE;
|
||||||
}
|
}
|
||||||
evaluation.format = newFormat;
|
evaluation.format = newFormat;
|
||||||
@ -236,8 +236,8 @@ public interface FormatEvaluator {
|
|||||||
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
|
: queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
|
||||||
Format current = evaluation.format;
|
Format current = evaluation.format;
|
||||||
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
|
Format ideal = determineIdealFormat(formats, bandwidthMeter.getEstimate());
|
||||||
boolean isHigher = ideal != null && current != null && ideal.bandwidth > current.bandwidth;
|
boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
|
||||||
boolean isLower = ideal != null && current != null && ideal.bandwidth < current.bandwidth;
|
boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
|
||||||
if (isHigher) {
|
if (isHigher) {
|
||||||
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
|
if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
|
||||||
// The ideal format is a higher quality, but we have insufficient buffer to
|
// The ideal format is a higher quality, but we have insufficient buffer to
|
||||||
@ -247,11 +247,11 @@ public interface FormatEvaluator {
|
|||||||
// We're switching from an SD stream to a stream of higher resolution. Consider
|
// We're switching from an SD stream to a stream of higher resolution. Consider
|
||||||
// discarding already buffered media chunks. Specifically, discard media chunks starting
|
// discarding already buffered media chunks. Specifically, discard media chunks starting
|
||||||
// from the first one that is of lower bandwidth, lower resolution and that is not HD.
|
// from the first one that is of lower bandwidth, lower resolution and that is not HD.
|
||||||
for (int i = 0; i < queue.size(); i++) {
|
for (int i = 1; i < queue.size(); i++) {
|
||||||
MediaChunk thisChunk = queue.get(i);
|
MediaChunk thisChunk = queue.get(i);
|
||||||
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
|
long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
|
||||||
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
|
if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
|
||||||
&& thisChunk.format.bandwidth < ideal.bandwidth
|
&& thisChunk.format.bitrate < ideal.bitrate
|
||||||
&& thisChunk.format.height < ideal.height
|
&& thisChunk.format.height < ideal.height
|
||||||
&& thisChunk.format.height < 720
|
&& thisChunk.format.height < 720
|
||||||
&& thisChunk.format.width < 1280) {
|
&& thisChunk.format.width < 1280) {
|
||||||
@ -280,7 +280,7 @@ public interface FormatEvaluator {
|
|||||||
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
|
long effectiveBandwidth = computeEffectiveBandwidthEstimate(bandwidthEstimate);
|
||||||
for (int i = 0; i < formats.length; i++) {
|
for (int i = 0; i < formats.length; i++) {
|
||||||
Format format = formats[i];
|
Format format = formats[i];
|
||||||
if (format.bandwidth <= effectiveBandwidth) {
|
if ((format.bitrate / 8) <= effectiveBandwidth) {
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,9 +73,7 @@ public abstract class MediaChunk extends Chunk {
|
|||||||
/**
|
/**
|
||||||
* Seeks to the beginning of the chunk.
|
* Seeks to the beginning of the chunk.
|
||||||
*/
|
*/
|
||||||
public final void seekToStart() {
|
public abstract void seekToStart();
|
||||||
seekTo(startTimeUs, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to the specified position within the chunk.
|
* Seeks to the specified position within the chunk.
|
||||||
@ -89,8 +87,22 @@ public abstract class MediaChunk extends Chunk {
|
|||||||
*/
|
*/
|
||||||
public abstract boolean seekTo(long positionUs, boolean allowNoop);
|
public abstract boolean seekTo(long positionUs, boolean allowNoop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the chunk for reading. Does nothing if the chunk is already prepared.
|
||||||
|
* <p>
|
||||||
|
* Preparation may require consuming some of the chunk. If the data is not yet available then
|
||||||
|
* this method will return {@code false} rather than block. The method can be called repeatedly
|
||||||
|
* until the return value indicates success.
|
||||||
|
*
|
||||||
|
* @return True if the chunk was prepared. False otherwise.
|
||||||
|
* @throws ParserException If an error occurs parsing the media data.
|
||||||
|
*/
|
||||||
|
public abstract boolean prepare() throws ParserException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the next media sample from the chunk.
|
* Reads the next media sample from the chunk.
|
||||||
|
* <p>
|
||||||
|
* Should only be called after the chunk has been successfully prepared.
|
||||||
*
|
*
|
||||||
* @param holder A holder to store the read sample.
|
* @param holder A holder to store the read sample.
|
||||||
* @return True if a sample was read. False if more data is still required.
|
* @return True if a sample was read. False if more data is still required.
|
||||||
@ -101,6 +113,8 @@ public abstract class MediaChunk extends Chunk {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the media format of the samples contained within this chunk.
|
* Returns the media format of the samples contained within this chunk.
|
||||||
|
* <p>
|
||||||
|
* Should only be called after the chunk has been successfully prepared.
|
||||||
*
|
*
|
||||||
* @return The sample media format.
|
* @return The sample media format.
|
||||||
*/
|
*/
|
||||||
@ -108,6 +122,8 @@ public abstract class MediaChunk extends Chunk {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the pssh information associated with the chunk.
|
* Returns the pssh information associated with the chunk.
|
||||||
|
* <p>
|
||||||
|
* Should only be called after the chunk has been successfully prepared.
|
||||||
*
|
*
|
||||||
* @return The pssh information.
|
* @return The pssh information.
|
||||||
*/
|
*/
|
||||||
|
@ -33,27 +33,43 @@ import java.util.UUID;
|
|||||||
public final class Mp4MediaChunk extends MediaChunk {
|
public final class Mp4MediaChunk extends MediaChunk {
|
||||||
|
|
||||||
private final FragmentedMp4Extractor extractor;
|
private final FragmentedMp4Extractor extractor;
|
||||||
|
private final boolean maybeSelfContained;
|
||||||
private final long sampleOffsetUs;
|
private final long sampleOffsetUs;
|
||||||
|
|
||||||
|
private boolean prepared;
|
||||||
|
private MediaFormat mediaFormat;
|
||||||
|
private Map<UUID, byte[]> psshInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param dataSource A {@link DataSource} for loading the data.
|
* @param dataSource A {@link DataSource} for loading the data.
|
||||||
* @param dataSpec Defines the data to be loaded.
|
* @param dataSpec Defines the data to be loaded.
|
||||||
* @param format The format of the stream to which this chunk belongs.
|
* @param 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 trigger The reason for this chunk being selected.
|
||||||
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
|
||||||
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
|
||||||
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
|
|
||||||
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
|
||||||
|
* @param extractor The extractor that will be used to extract the samples.
|
||||||
|
* @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
|
||||||
|
* contain a moov atom defining the media format of the chunk. This parameter can always be
|
||||||
|
* safely set to true. Setting to false where the chunk is known to not be self contained may
|
||||||
|
* improve startup latency.
|
||||||
|
* @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
|
||||||
*/
|
*/
|
||||||
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
|
||||||
int trigger, FragmentedMp4Extractor extractor, long startTimeUs, long endTimeUs,
|
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
|
||||||
long sampleOffsetUs, int nextChunkIndex) {
|
FragmentedMp4Extractor 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.sampleOffsetUs = sampleOffsetUs;
|
this.sampleOffsetUs = sampleOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seekToStart() {
|
||||||
|
extractor.seekTo(0, false);
|
||||||
|
resetReadPosition();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||||
long seekTimeUs = positionUs + sampleOffsetUs;
|
long seekTimeUs = positionUs + sampleOffsetUs;
|
||||||
@ -64,6 +80,29 @@ public final class Mp4MediaChunk extends MediaChunk {
|
|||||||
return isDiscontinuous;
|
return isDiscontinuous;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean prepare() throws ParserException {
|
||||||
|
if (!prepared) {
|
||||||
|
if (maybeSelfContained) {
|
||||||
|
// Read up to the first sample. Once we're there, we know that the extractor must have
|
||||||
|
// parsed a moov atom if the chunk contains one.
|
||||||
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
|
Assertions.checkState(inputStream != null);
|
||||||
|
int result = extractor.read(inputStream, null);
|
||||||
|
prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
|
||||||
|
} else {
|
||||||
|
// We know there isn't a moov atom. The extractor must have parsed one from a separate
|
||||||
|
// initialization chunk.
|
||||||
|
prepared = true;
|
||||||
|
}
|
||||||
|
if (prepared) {
|
||||||
|
mediaFormat = Assertions.checkNotNull(extractor.getFormat());
|
||||||
|
psshInfo = extractor.getPsshInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean read(SampleHolder holder) throws ParserException {
|
public boolean read(SampleHolder holder) throws ParserException {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
@ -78,12 +117,12 @@ public final class Mp4MediaChunk extends MediaChunk {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaFormat getMediaFormat() {
|
public MediaFormat getMediaFormat() {
|
||||||
return extractor.getFormat();
|
return mediaFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<UUID, byte[]> getPsshInfo() {
|
public Map<UUID, byte[]> getPsshInfo() {
|
||||||
return extractor.getPsshInfo();
|
return psshInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable(List<MediaChunk> queue) {
|
public void disable(List<? extends MediaChunk> queue) {
|
||||||
selectedSource.disable(queue);
|
selectedSource.disable(queue);
|
||||||
enabled = false;
|
enabled = false;
|
||||||
}
|
}
|
||||||
@ -102,4 +102,9 @@ public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkLoadError(Chunk chunk, Exception e) {
|
||||||
|
selectedSource.onChunkLoadError(chunk, e);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
|
|||||||
this.headerData = headerData;
|
this.headerData = headerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean prepare() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean read(SampleHolder holder) {
|
public boolean read(SampleHolder holder) {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
@ -109,6 +114,11 @@ public class SingleSampleMediaChunk extends MediaChunk {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seekToStart() {
|
||||||
|
resetReadPosition();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||||
resetReadPosition();
|
resetReadPosition();
|
||||||
|
@ -50,6 +50,11 @@ public final class WebmMediaChunk extends MediaChunk {
|
|||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seekToStart() {
|
||||||
|
seekTo(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean seekTo(long positionUs, boolean allowNoop) {
|
public boolean seekTo(long positionUs, boolean allowNoop) {
|
||||||
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
|
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
|
||||||
@ -59,6 +64,11 @@ public final class WebmMediaChunk extends MediaChunk {
|
|||||||
return isDiscontinuous;
|
return isDiscontinuous;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean prepare() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean read(SampleHolder holder) {
|
public boolean read(SampleHolder holder) {
|
||||||
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
NonBlockingInputStream inputStream = getNonBlockingInputStream();
|
||||||
|
@ -27,18 +27,18 @@ 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.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.SegmentIndex;
|
|
||||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||||
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 android.util.Log;
|
import android.net.Uri;
|
||||||
import android.util.SparseArray;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,26 +46,17 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class DashMp4ChunkSource implements ChunkSource {
|
public class DashMp4ChunkSource implements ChunkSource {
|
||||||
|
|
||||||
public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1;
|
|
||||||
|
|
||||||
private static final int EXPECTED_INITIALIZATION_RESULT =
|
|
||||||
FragmentedMp4Extractor.RESULT_END_OF_STREAM
|
|
||||||
| FragmentedMp4Extractor.RESULT_READ_MOOV
|
|
||||||
| FragmentedMp4Extractor.RESULT_READ_SIDX;
|
|
||||||
|
|
||||||
private static final String TAG = "DashMp4ChunkSource";
|
|
||||||
|
|
||||||
private final TrackInfo trackInfo;
|
private final TrackInfo trackInfo;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final FormatEvaluator evaluator;
|
private final FormatEvaluator evaluator;
|
||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
private final int maxWidth;
|
private final int maxWidth;
|
||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
private final int numSegmentsPerChunk;
|
|
||||||
|
|
||||||
private final Format[] formats;
|
private final Format[] formats;
|
||||||
private final SparseArray<Representation> representations;
|
private final HashMap<String, Representation> representations;
|
||||||
private final SparseArray<FragmentedMp4Extractor> extractors;
|
private final HashMap<String, FragmentedMp4Extractor> extractors;
|
||||||
|
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
|
|
||||||
@ -76,26 +67,14 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
*/
|
*/
|
||||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||||
Representation... representations) {
|
Representation... representations) {
|
||||||
this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
|
||||||
* @param evaluator Selects from the available formats.
|
|
||||||
* @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index)
|
|
||||||
* that should be grouped into a single chunk.
|
|
||||||
* @param representations The representations to be considered by the source.
|
|
||||||
*/
|
|
||||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
|
||||||
int numSegmentsPerChunk, Representation... representations) {
|
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = evaluator;
|
this.evaluator = evaluator;
|
||||||
this.numSegmentsPerChunk = numSegmentsPerChunk;
|
|
||||||
this.formats = new Format[representations.length];
|
this.formats = new Format[representations.length];
|
||||||
this.extractors = new SparseArray<FragmentedMp4Extractor>();
|
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
|
||||||
this.representations = new SparseArray<Representation>();
|
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||||
|
this.representations = new HashMap<String, Representation>();
|
||||||
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||||
representations[0].periodDuration * 1000);
|
representations[0].periodDurationMs * 1000);
|
||||||
this.evaluation = new Evaluation();
|
this.evaluation = new Evaluation();
|
||||||
int maxWidth = 0;
|
int maxWidth = 0;
|
||||||
int maxHeight = 0;
|
int maxHeight = 0;
|
||||||
@ -103,8 +82,12 @@ 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.append(formats[i].id, new FragmentedMp4Extractor());
|
extractors.put(formats[i].id, new FragmentedMp4Extractor());
|
||||||
this.representations.put(formats[i].id, representations[i]);
|
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.maxWidth = maxWidth;
|
||||||
this.maxHeight = maxHeight;
|
this.maxHeight = maxHeight;
|
||||||
@ -129,7 +112,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable(List<MediaChunk> queue) {
|
public void disable(List<? extends MediaChunk> queue) {
|
||||||
evaluator.disable();
|
evaluator.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +135,7 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
} else if (out.queueSize == queue.size() && out.chunk != null
|
} else if (out.queueSize == queue.size() && out.chunk != null
|
||||||
&& out.chunk.format.id == selectedFormat.id) {
|
&& out.chunk.format.id.equals(selectedFormat.id)) {
|
||||||
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
||||||
// of the queue. Leave unchanged.
|
// of the queue. Leave unchanged.
|
||||||
return;
|
return;
|
||||||
@ -160,29 +143,39 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||||
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||||
|
|
||||||
|
RangedUri pendingInitializationUri = null;
|
||||||
|
RangedUri pendingIndexUri = null;
|
||||||
if (extractor.getTrack() == null) {
|
if (extractor.getTrack() == null) {
|
||||||
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
|
pendingInitializationUri = selectedRepresentation.getInitializationUri();
|
||||||
dataSource, evaluation.trigger);
|
}
|
||||||
|
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;
|
lastChunkWasInitialization = true;
|
||||||
out.chunk = initializationChunk;
|
out.chunk = initializationChunk;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nextIndex;
|
int nextSegmentNum;
|
||||||
|
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().timesUs, seekPositionUs);
|
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
||||||
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
|
|
||||||
} else {
|
} else {
|
||||||
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
|
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex == -1) {
|
if (nextSegmentNum == -1) {
|
||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
|
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
|
||||||
extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
|
dataSource, nextSegmentNum, evaluation.trigger);
|
||||||
lastChunkWasInitialization = false;
|
lastChunkWasInitialization = false;
|
||||||
out.chunk = nextMediaChunk;
|
out.chunk = nextMediaChunk;
|
||||||
}
|
}
|
||||||
@ -192,75 +185,80 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newInitializationChunk(Representation representation,
|
@Override
|
||||||
FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) {
|
public void onChunkLoadError(Chunk chunk, Exception e) {
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
|
// Do nothing.
|
||||||
representation.getCacheKey());
|
|
||||||
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newMediaChunk(Representation representation,
|
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||||
FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index,
|
Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
|
||||||
int trigger, int numSegmentsPerChunk) {
|
int trigger) {
|
||||||
|
int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM;
|
||||||
// Computes the segments to included in the next fetch.
|
long indexAnchor = 0;
|
||||||
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index);
|
RangedUri requestUri;
|
||||||
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
|
if (initializationUri != null) {
|
||||||
int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1;
|
// It's common for initialization and index data to be stored adjacently. Attempt to merge
|
||||||
|
// the two requests together to request both at once.
|
||||||
long startTimeUs = sidx.timesUs[index];
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV;
|
||||||
|
requestUri = initializationUri.attemptMerge(indexUri);
|
||||||
// Compute the end time, prefer to use next segment start time if there is a next segment.
|
if (requestUri != null) {
|
||||||
long endTimeUs = nextIndex == -1 ?
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
|
||||||
sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] :
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
sidx.timesUs[nextIndex];
|
} else {
|
||||||
|
requestUri = initializationUri;
|
||||||
long offset = (int) representation.indexEnd + 1 + sidx.offsets[index];
|
}
|
||||||
|
} else {
|
||||||
// Compute combined segments byte length.
|
requestUri = indexUri;
|
||||||
long size = 0;
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
for (int i = index; i <= lastSegmentInChunk; i++) {
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
|
||||||
size += sidx.sizes[i];
|
|
||||||
}
|
}
|
||||||
|
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
|
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
|
||||||
startTimeUs, endTimeUs, 0, nextIndex);
|
extractor, expectedExtractorResult, indexAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class InitializationMp4Loadable extends Chunk {
|
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||||
|
FragmentedMp4Extractor 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 Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
|
||||||
|
endTimeUs, nextSegmentNum, extractor, false, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InitializationMp4Loadable extends Chunk {
|
||||||
|
|
||||||
private final Representation representation;
|
|
||||||
private final FragmentedMp4Extractor extractor;
|
private final FragmentedMp4Extractor extractor;
|
||||||
|
private final int expectedExtractorResult;
|
||||||
|
private final long indexAnchor;
|
||||||
|
private final Uri uri;
|
||||||
|
|
||||||
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||||
FragmentedMp4Extractor extractor, Representation representation) {
|
Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
|
||||||
super(dataSource, dataSpec, representation.format, trigger);
|
long indexAnchor) {
|
||||||
|
super(dataSource, dataSpec, format, trigger);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
this.representation = representation;
|
this.expectedExtractorResult = expectedExtractorResult;
|
||||||
|
this.indexAnchor = indexAnchor;
|
||||||
|
this.uri = dataSpec.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||||
int result = extractor.read(stream, null);
|
int result = extractor.read(stream, null);
|
||||||
if (result != EXPECTED_INITIALIZATION_RESULT) {
|
if (result != expectedExtractorResult) {
|
||||||
throw new ParserException("Invalid initialization data");
|
throw new ParserException("Invalid extractor result. Expected "
|
||||||
|
+ expectedExtractorResult + ", got " + result);
|
||||||
}
|
}
|
||||||
validateSegmentIndex(extractor.getSegmentIndex());
|
if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) {
|
||||||
}
|
segmentIndexes.put(format.id,
|
||||||
|
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor));
|
||||||
private void validateSegmentIndex(SegmentIndex segmentIndex) {
|
|
||||||
long expectedIndexLen = representation.indexEnd - representation.indexStart + 1;
|
|
||||||
if (segmentIndex.sizeBytes != expectedIndexLen) {
|
|
||||||
Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes +
|
|
||||||
", ExpectedLen = " + expectedIndexLen);
|
|
||||||
}
|
|
||||||
long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] +
|
|
||||||
segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1;
|
|
||||||
if (sidxContentLength != representation.contentLength) {
|
|
||||||
Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength +
|
|
||||||
", Expected = " + representation.contentLength);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,18 +27,19 @@ 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.WebmMediaChunk;
|
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.dash.mpd.Representation;
|
||||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
|
||||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
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 android.util.Log;
|
import android.net.Uri;
|
||||||
import android.util.SparseArray;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,37 +47,30 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class DashWebmChunkSource implements ChunkSource {
|
public class DashWebmChunkSource implements ChunkSource {
|
||||||
|
|
||||||
private static final String TAG = "DashWebmChunkSource";
|
|
||||||
|
|
||||||
private final TrackInfo trackInfo;
|
private final TrackInfo trackInfo;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final FormatEvaluator evaluator;
|
private final FormatEvaluator evaluator;
|
||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
private final int maxWidth;
|
private final int maxWidth;
|
||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
private final int numSegmentsPerChunk;
|
|
||||||
|
|
||||||
private final Format[] formats;
|
private final Format[] formats;
|
||||||
private final SparseArray<Representation> representations;
|
private final HashMap<String, Representation> representations;
|
||||||
private final SparseArray<WebmExtractor> extractors;
|
private final HashMap<String, WebmExtractor> extractors;
|
||||||
|
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
|
|
||||||
public DashWebmChunkSource(
|
|
||||||
DataSource dataSource, FormatEvaluator evaluator, Representation... representations) {
|
|
||||||
this(dataSource, evaluator, 1, representations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||||
int numSegmentsPerChunk, Representation... representations) {
|
Representation... representations) {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = evaluator;
|
this.evaluator = evaluator;
|
||||||
this.numSegmentsPerChunk = numSegmentsPerChunk;
|
|
||||||
this.formats = new Format[representations.length];
|
this.formats = new Format[representations.length];
|
||||||
this.extractors = new SparseArray<WebmExtractor>();
|
this.extractors = new HashMap<String, WebmExtractor>();
|
||||||
this.representations = new SparseArray<Representation>();
|
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||||
|
this.representations = new HashMap<String, Representation>();
|
||||||
this.trackInfo = new TrackInfo(
|
this.trackInfo = new TrackInfo(
|
||||||
representations[0].format.mimeType, representations[0].periodDuration * 1000);
|
representations[0].format.mimeType, representations[0].periodDurationMs * 1000);
|
||||||
this.evaluation = new Evaluation();
|
this.evaluation = new Evaluation();
|
||||||
int maxWidth = 0;
|
int maxWidth = 0;
|
||||||
int maxHeight = 0;
|
int maxHeight = 0;
|
||||||
@ -84,8 +78,12 @@ public class DashWebmChunkSource 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.append(formats[i].id, new WebmExtractor());
|
extractors.put(formats[i].id, new DefaultWebmExtractor());
|
||||||
this.representations.put(formats[i].id, representations[i]);
|
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.maxWidth = maxWidth;
|
||||||
this.maxHeight = maxHeight;
|
this.maxHeight = maxHeight;
|
||||||
@ -110,7 +108,7 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable(List<MediaChunk> queue) {
|
public void disable(List<? extends MediaChunk> queue) {
|
||||||
evaluator.disable();
|
evaluator.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +131,7 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
} else if (out.queueSize == queue.size() && out.chunk != null
|
} else if (out.queueSize == queue.size() && out.chunk != null
|
||||||
&& out.chunk.format.id == selectedFormat.id) {
|
&& out.chunk.format.id.equals(selectedFormat.id)) {
|
||||||
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
||||||
// of the queue. Leave unchanged.
|
// of the queue. Leave unchanged.
|
||||||
return;
|
return;
|
||||||
@ -141,29 +139,34 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||||
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||||
|
|
||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
|
// TODO: This code forces cues to exist and to immediately follow the initialization
|
||||||
dataSource, evaluation.trigger);
|
// data. Webm extractor should be generalized to allow cues to be optional. See [redacted].
|
||||||
|
RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge(
|
||||||
|
selectedRepresentation.getIndexUri());
|
||||||
|
Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation,
|
||||||
|
extractor, dataSource, evaluation.trigger);
|
||||||
lastChunkWasInitialization = true;
|
lastChunkWasInitialization = true;
|
||||||
out.chunk = initializationChunk;
|
out.chunk = initializationChunk;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nextIndex;
|
int nextSegmentNum;
|
||||||
|
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs);
|
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
||||||
nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex;
|
|
||||||
} else {
|
} else {
|
||||||
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
|
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex == -1) {
|
if (nextSegmentNum == -1) {
|
||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
|
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
|
||||||
extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
|
dataSource, nextSegmentNum, evaluation.trigger);
|
||||||
lastChunkWasInitialization = false;
|
lastChunkWasInitialization = false;
|
||||||
out.chunk = nextMediaChunk;
|
out.chunk = nextMediaChunk;
|
||||||
}
|
}
|
||||||
@ -173,53 +176,43 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newInitializationChunk(Representation representation,
|
@Override
|
||||||
WebmExtractor extractor, DataSource dataSource, int trigger) {
|
public void onChunkLoadError(Chunk chunk, Exception e) {
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
|
// Do nothing.
|
||||||
representation.getCacheKey());
|
|
||||||
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newMediaChunk(Representation representation,
|
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation,
|
||||||
WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index,
|
WebmExtractor extractor, DataSource dataSource, int trigger) {
|
||||||
int trigger, int numSegmentsPerChunk) {
|
DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start,
|
||||||
|
initializationUri.length, representation.getCacheKey());
|
||||||
|
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
|
||||||
|
extractor);
|
||||||
|
}
|
||||||
|
|
||||||
// Computes the segments to included in the next fetch.
|
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||||
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index);
|
WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
|
||||||
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
|
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1;
|
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
||||||
|
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
||||||
long startTimeUs = cues.timesUs[index];
|
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
|
||||||
|
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
|
||||||
// Compute the end time, prefer to use next segment start time if there is a next segment.
|
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
|
||||||
long endTimeUs = nextIndex == -1 ?
|
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
|
||||||
cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] :
|
|
||||||
cues.timesUs[nextIndex];
|
|
||||||
|
|
||||||
long offset = cues.offsets[index];
|
|
||||||
|
|
||||||
// Compute combined segments byte length.
|
|
||||||
long size = 0;
|
|
||||||
for (int i = index; i <= lastSegmentInChunk; i++) {
|
|
||||||
size += cues.sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
|
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
||||||
startTimeUs, endTimeUs, nextIndex);
|
startTimeUs, endTimeUs, nextSegmentNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class InitializationWebmLoadable extends Chunk {
|
private class InitializationWebmLoadable extends Chunk {
|
||||||
|
|
||||||
private final Representation representation;
|
|
||||||
private final WebmExtractor extractor;
|
private final WebmExtractor extractor;
|
||||||
|
private final Uri uri;
|
||||||
|
|
||||||
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||||
WebmExtractor extractor, Representation representation) {
|
Format format, WebmExtractor extractor) {
|
||||||
super(dataSource, dataSpec, representation.format, trigger);
|
super(dataSource, dataSpec, format, trigger);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
this.representation = representation;
|
this.uri = dataSpec.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -228,22 +221,7 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
throw new ParserException("Invalid initialization data");
|
throw new ParserException("Invalid initialization data");
|
||||||
}
|
}
|
||||||
validateCues(extractor.getCues());
|
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0));
|
||||||
}
|
|
||||||
|
|
||||||
private void validateCues(SegmentIndex cues) {
|
|
||||||
long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1;
|
|
||||||
if (cues.sizeBytes != expectedSizeBytes) {
|
|
||||||
Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes +
|
|
||||||
" but expected " + expectedSizeBytes);
|
|
||||||
}
|
|
||||||
long expectedContentLength = cues.offsets[cues.length - 1] +
|
|
||||||
cues.sizes[cues.length - 1] + representation.indexEnd + 1;
|
|
||||||
if (representation.contentLength > 0
|
|
||||||
&& expectedContentLength != representation.contentLength) {
|
|
||||||
Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength +
|
|
||||||
" but expected " + representation.contentLength);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd;
|
|||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
||||||
String contentId) throws IOException, ParserException {
|
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||||
try {
|
try {
|
||||||
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId);
|
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
|
||||||
} catch (XmlPullParserException e) {
|
} catch (XmlPullParserException e) {
|
||||||
throw new ParserException(e);
|
throw new ParserException(e);
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,15 @@ package com.google.android.exoplayer.dash.mpd;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.chunk.Format;
|
import com.google.android.exoplayer.chunk.Format;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.xml.sax.helpers.DefaultHandler;
|
import org.xml.sax.helpers.DefaultHandler;
|
||||||
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
@ -38,15 +42,8 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* A parser of media presentation description files.
|
* A parser of media presentation description files.
|
||||||
*/
|
*/
|
||||||
/*
|
|
||||||
* TODO: Parse representation base attributes at multiple levels, and normalize the resulting
|
|
||||||
* datastructure.
|
|
||||||
* TODO: Decide how best to represent missing integer/double/long attributes.
|
|
||||||
*/
|
|
||||||
public class MediaPresentationDescriptionParser extends DefaultHandler {
|
public class MediaPresentationDescriptionParser extends DefaultHandler {
|
||||||
|
|
||||||
private static final String TAG = "MediaPresentationDescriptionParser";
|
|
||||||
|
|
||||||
// Note: Does not support the date part of ISO 8601
|
// Note: Does not support the date part of ISO 8601
|
||||||
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)?$");
|
||||||
@ -61,20 +58,23 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MPD parsing.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a manifest from the provided {@link InputStream}.
|
* Parses a manifest from the provided {@link InputStream}.
|
||||||
*
|
*
|
||||||
* @param inputStream The stream from which to parse the manifest.
|
* @param inputStream The stream from which to parse the manifest.
|
||||||
* @param inputEncoding The encoding of the input.
|
* @param inputEncoding The encoding of the input.
|
||||||
* @param contentId The content id of the media.
|
* @param contentId The content id of the media.
|
||||||
|
* @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 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) throws XmlPullParserException, IOException,
|
String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
|
||||||
ParserException {
|
IOException, ParserException {
|
||||||
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
||||||
xpp.setInput(inputStream, inputEncoding);
|
xpp.setInput(inputStream, inputEncoding);
|
||||||
int eventType = xpp.next();
|
int eventType = xpp.next();
|
||||||
@ -82,123 +82,139 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
throw new ParserException(
|
throw new ParserException(
|
||||||
"inputStream does not contain a valid media presentation description");
|
"inputStream does not contain a valid media presentation description");
|
||||||
}
|
}
|
||||||
return parseMediaPresentationDescription(xpp, contentId);
|
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
||||||
String contentId) throws XmlPullParserException, IOException {
|
String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
|
||||||
long duration = parseDurationMs(xpp, "mediaPresentationDuration");
|
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
|
||||||
long minBufferTime = 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 minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
||||||
|
|
||||||
List<Period> periods = new ArrayList<Period>();
|
List<Period> periods = new ArrayList<Period>();
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
if (isStartTag(xpp, "Period")) {
|
if (isStartTag(xpp, "BaseURL")) {
|
||||||
periods.add(parsePeriod(xpp, contentId, duration));
|
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||||
|
} else if (isStartTag(xpp, "Period")) {
|
||||||
|
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
|
||||||
}
|
}
|
||||||
} while (!isEndTag(xpp, "MPD"));
|
} while (!isEndTag(xpp, "MPD"));
|
||||||
|
|
||||||
return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime,
|
return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
|
||||||
periods);
|
periods);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration)
|
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
int id = parseInt(xpp, "id");
|
String id = xpp.getAttributeValue(null, "id");
|
||||||
long start = parseDurationMs(xpp, "start", 0);
|
long startMs = parseDurationMs(xpp, "start", 0);
|
||||||
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
|
long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs);
|
||||||
|
SegmentBase segmentBase = null;
|
||||||
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
|
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
|
||||||
List<Segment.Timeline> segmentTimelineList = null;
|
|
||||||
int segmentStartNumber = 0;
|
|
||||||
int segmentTimescale = 0;
|
|
||||||
long presentationTimeOffset = 0;
|
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
if (isStartTag(xpp, "AdaptationSet")) {
|
if (isStartTag(xpp, "BaseURL")) {
|
||||||
adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration,
|
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||||
segmentTimelineList));
|
} else if (isStartTag(xpp, "AdaptationSet")) {
|
||||||
|
adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs,
|
||||||
|
segmentBase));
|
||||||
|
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||||
|
segmentBase = parseSegmentBase(xpp, baseUrl, null);
|
||||||
} else if (isStartTag(xpp, "SegmentList")) {
|
} else if (isStartTag(xpp, "SegmentList")) {
|
||||||
segmentStartNumber = parseInt(xpp, "startNumber");
|
segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs);
|
||||||
segmentTimescale = parseInt(xpp, "timescale");
|
} else if (isStartTag(xpp, "SegmentTemplate")) {
|
||||||
presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
|
segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs);
|
||||||
segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
|
|
||||||
}
|
}
|
||||||
} while (!isEndTag(xpp, "Period"));
|
} while (!isEndTag(xpp, "Period"));
|
||||||
|
|
||||||
return new Period(id, start, duration, adaptationSets, segmentTimelineList,
|
return new Period(id, startMs, durationMs, adaptationSets);
|
||||||
segmentStartNumber, segmentTimescale, presentationTimeOffset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Segment.Timeline> parsePeriodSegmentList(
|
// AdaptationSet parsing.
|
||||||
XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
|
|
||||||
List<Segment.Timeline> segmentTimelineList = new ArrayList<Segment.Timeline>();
|
|
||||||
|
|
||||||
do {
|
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl,
|
||||||
xpp.next();
|
long periodStartMs, long periodDurationMs, SegmentBase segmentBase)
|
||||||
if (isStartTag(xpp, "SegmentTimeline")) {
|
|
||||||
do {
|
|
||||||
xpp.next();
|
|
||||||
if (isStartTag(xpp, "S")) {
|
|
||||||
long duration = parseLong(xpp, "d");
|
|
||||||
segmentTimelineList.add(new Segment.Timeline(segmentStartNumber, duration));
|
|
||||||
segmentStartNumber++;
|
|
||||||
}
|
|
||||||
} while (!isEndTag(xpp, "SegmentTimeline"));
|
|
||||||
}
|
|
||||||
} while (!isEndTag(xpp, "SegmentList"));
|
|
||||||
|
|
||||||
return segmentTimelineList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart,
|
|
||||||
long periodDuration, List<Segment.Timeline> segmentTimelineList)
|
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
int id = -1;
|
|
||||||
int contentType = AdaptationSet.TYPE_UNKNOWN;
|
|
||||||
|
|
||||||
// TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9.
|
|
||||||
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
||||||
if (mimeType != null) {
|
int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
|
||||||
if (MimeTypes.isAudio(mimeType)) {
|
|
||||||
contentType = AdaptationSet.TYPE_AUDIO;
|
|
||||||
} else if (MimeTypes.isVideo(mimeType)) {
|
|
||||||
contentType = AdaptationSet.TYPE_VIDEO;
|
|
||||||
} else if (MimeTypes.isText(mimeType)
|
|
||||||
|| mimeType.equalsIgnoreCase(MimeTypes.APPLICATION_TTML)) {
|
|
||||||
contentType = AdaptationSet.TYPE_TEXT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
int id = -1;
|
||||||
List<ContentProtection> contentProtections = null;
|
List<ContentProtection> contentProtections = null;
|
||||||
List<Representation> representations = new ArrayList<Representation>();
|
List<Representation> representations = new ArrayList<Representation>();
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
if (contentType != AdaptationSet.TYPE_UNKNOWN) {
|
if (isStartTag(xpp, "BaseURL")) {
|
||||||
if (isStartTag(xpp, "ContentProtection")) {
|
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||||
if (contentProtections == null) {
|
} else if (isStartTag(xpp, "ContentProtection")) {
|
||||||
contentProtections = new ArrayList<ContentProtection>();
|
if (contentProtections == null) {
|
||||||
}
|
contentProtections = new ArrayList<ContentProtection>();
|
||||||
contentProtections.add(parseContentProtection(xpp));
|
|
||||||
} else if (isStartTag(xpp, "ContentComponent")) {
|
|
||||||
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
|
|
||||||
String contentTypeString = xpp.getAttributeValue(null, "contentType");
|
|
||||||
contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO
|
|
||||||
: "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
|
|
||||||
: AdaptationSet.TYPE_UNKNOWN;
|
|
||||||
} else if (isStartTag(xpp, "Representation")) {
|
|
||||||
representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration,
|
|
||||||
mimeType, segmentTimelineList));
|
|
||||||
}
|
}
|
||||||
|
contentProtections.add(parseContentProtection(xpp));
|
||||||
|
} else if (isStartTag(xpp, "ContentComponent")) {
|
||||||
|
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
|
||||||
|
contentType = checkAdaptationSetTypeConsistency(contentType,
|
||||||
|
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
|
||||||
|
} else if (isStartTag(xpp, "Representation")) {
|
||||||
|
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
|
||||||
|
periodDurationMs, mimeType, segmentBase);
|
||||||
|
contentType = checkAdaptationSetTypeConsistency(contentType,
|
||||||
|
parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
|
||||||
|
representations.add(representation);
|
||||||
|
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||||
|
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
|
||||||
|
} else if (isStartTag(xpp, "SegmentList")) {
|
||||||
|
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
|
||||||
|
} else if (isStartTag(xpp, "SegmentTemplate")) {
|
||||||
|
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
|
||||||
|
periodDurationMs);
|
||||||
}
|
}
|
||||||
} while (!isEndTag(xpp, "AdaptationSet"));
|
} while (!isEndTag(xpp, "AdaptationSet"));
|
||||||
|
|
||||||
return new AdaptationSet(id, contentType, representations, contentProtections);
|
return new AdaptationSet(id, contentType, representations, contentProtections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int parseAdaptationSetType(String contentType) {
|
||||||
|
return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN
|
||||||
|
: MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO
|
||||||
|
: MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO
|
||||||
|
: MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? AdaptationSet.TYPE_TEXT
|
||||||
|
: AdaptationSet.TYPE_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseAdaptationSetTypeFromMimeType(String mimeType) {
|
||||||
|
return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN
|
||||||
|
: MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO
|
||||||
|
: MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO
|
||||||
|
: MimeTypes.isText(mimeType) || MimeTypes.isTtml(mimeType) ? AdaptationSet.TYPE_TEXT
|
||||||
|
: AdaptationSet.TYPE_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks two adaptation set types for consistency, returning the consistent type, or throwing an
|
||||||
|
* {@link IllegalStateException} if the types are inconsistent.
|
||||||
|
* <p>
|
||||||
|
* Two types are consistent if they are equal, or if one is {@link AdaptationSet#TYPE_UNKNOWN}.
|
||||||
|
* Where one of the types is {@link AdaptationSet#TYPE_UNKNOWN}, the other is returned.
|
||||||
|
*
|
||||||
|
* @param firstType The first type.
|
||||||
|
* @param secondType The second type.
|
||||||
|
* @return The consistent type.
|
||||||
|
*/
|
||||||
|
private int checkAdaptationSetTypeConsistency(int firstType, int secondType) {
|
||||||
|
if (firstType == AdaptationSet.TYPE_UNKNOWN) {
|
||||||
|
return secondType;
|
||||||
|
} else if (secondType == AdaptationSet.TYPE_UNKNOWN) {
|
||||||
|
return firstType;
|
||||||
|
} else {
|
||||||
|
Assertions.checkState(firstType == secondType);
|
||||||
|
return firstType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a ContentProtection element.
|
* Parses a ContentProtection element.
|
||||||
*
|
*
|
||||||
@ -211,99 +227,194 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
return new ContentProtection(schemeUriId, null);
|
return new ContentProtection(schemeUriId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart,
|
// Representation parsing.
|
||||||
long periodDuration, String parentMimeType, List<Segment.Timeline> segmentTimelineList)
|
|
||||||
|
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
|
||||||
|
long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
int id;
|
String id = xpp.getAttributeValue(null, "id");
|
||||||
try {
|
int bandwidth = parseInt(xpp, "bandwidth");
|
||||||
id = parseInt(xpp, "id");
|
|
||||||
} catch (NumberFormatException nfe) {
|
|
||||||
Log.d(TAG, "Unable to parse id; " + nfe.getMessage());
|
|
||||||
// TODO: need a way to generate a unique and stable id; use hashCode for now
|
|
||||||
id = xpp.getAttributeValue(null, "id").hashCode();
|
|
||||||
}
|
|
||||||
int bandwidth = parseInt(xpp, "bandwidth") / 8;
|
|
||||||
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
|
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
|
||||||
int width = parseInt(xpp, "width");
|
int width = parseInt(xpp, "width");
|
||||||
int height = parseInt(xpp, "height");
|
int height = parseInt(xpp, "height");
|
||||||
|
mimeType = parseString(xpp, "mimeType", mimeType);
|
||||||
|
|
||||||
String mimeType = xpp.getAttributeValue(null, "mimeType");
|
|
||||||
if (mimeType == null) {
|
|
||||||
mimeType = parentMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
String representationUrl = null;
|
|
||||||
long indexStart = -1;
|
|
||||||
long indexEnd = -1;
|
|
||||||
long initializationStart = -1;
|
|
||||||
long initializationEnd = -1;
|
|
||||||
int numChannels = -1;
|
int numChannels = -1;
|
||||||
List<Segment> segmentList = null;
|
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
if (isStartTag(xpp, "BaseURL")) {
|
if (isStartTag(xpp, "BaseURL")) {
|
||||||
xpp.next();
|
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||||
representationUrl = xpp.getText();
|
|
||||||
} else if (isStartTag(xpp, "AudioChannelConfiguration")) {
|
} else if (isStartTag(xpp, "AudioChannelConfiguration")) {
|
||||||
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
|
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
|
||||||
} else if (isStartTag(xpp, "SegmentBase")) {
|
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||||
String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-");
|
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
|
||||||
indexStart = Long.parseLong(indexRange[0]);
|
|
||||||
indexEnd = Long.parseLong(indexRange[1]);
|
|
||||||
} else if (isStartTag(xpp, "SegmentList")) {
|
} else if (isStartTag(xpp, "SegmentList")) {
|
||||||
segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList);
|
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
|
||||||
} else if (isStartTag(xpp, "Initialization")) {
|
} else if (isStartTag(xpp, "SegmentTemplate")) {
|
||||||
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
|
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
|
||||||
initializationStart = Long.parseLong(indexRange[0]);
|
periodDurationMs);
|
||||||
initializationEnd = Long.parseLong(indexRange[1]);
|
|
||||||
}
|
}
|
||||||
} while (!isEndTag(xpp, "Representation"));
|
} while (!isEndTag(xpp, "Representation"));
|
||||||
|
|
||||||
Uri uri = Uri.parse(representationUrl);
|
|
||||||
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
|
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
|
||||||
bandwidth);
|
bandwidth);
|
||||||
if (segmentList == null) {
|
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
|
||||||
return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED,
|
segmentBase);
|
||||||
initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
|
|
||||||
periodDuration);
|
|
||||||
} else {
|
|
||||||
return new SegmentedRepresentation(contentId, format, uri, initializationStart,
|
|
||||||
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Segment> parseRepresentationSegmentList(XmlPullParser xpp,
|
// SegmentBase, SegmentList and SegmentTemplate parsing.
|
||||||
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
|
|
||||||
List<Segment> segmentList = new ArrayList<Segment>();
|
private SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl,
|
||||||
int i = 0;
|
SingleSegmentBase parent) throws XmlPullParserException, IOException {
|
||||||
|
|
||||||
|
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||||
|
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||||
|
parent != null ? parent.presentationTimeOffset : 0);
|
||||||
|
|
||||||
|
long indexStart = parent != null ? parent.indexStart : 0;
|
||||||
|
long indexLength = parent != null ? parent.indexLength : -1;
|
||||||
|
String indexRangeText = xpp.getAttributeValue(null, "indexRange");
|
||||||
|
if (indexRangeText != null) {
|
||||||
|
String[] indexRange = indexRangeText.split("-");
|
||||||
|
indexStart = Long.parseLong(indexRange[0]);
|
||||||
|
indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RangedUri initialization = parent != null ? parent.initialization : null;
|
||||||
|
do {
|
||||||
|
xpp.next();
|
||||||
|
if (isStartTag(xpp, "Initialization")) {
|
||||||
|
initialization = parseInitialization(xpp, baseUrl);
|
||||||
|
}
|
||||||
|
} while (!isEndTag(xpp, "SegmentBase"));
|
||||||
|
|
||||||
|
return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
|
||||||
|
indexStart, indexLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent,
|
||||||
|
long periodDuration) throws XmlPullParserException, IOException {
|
||||||
|
|
||||||
|
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||||
|
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||||
|
parent != null ? parent.presentationTimeOffset : 0);
|
||||||
|
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
|
||||||
|
int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0);
|
||||||
|
|
||||||
|
RangedUri initialization = null;
|
||||||
|
List<SegmentTimelineElement> timeline = null;
|
||||||
|
List<RangedUri> segments = null;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
if (isStartTag(xpp, "Initialization")) {
|
if (isStartTag(xpp, "Initialization")) {
|
||||||
String url = xpp.getAttributeValue(null, "sourceURL");
|
initialization = parseInitialization(xpp, baseUrl);
|
||||||
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
|
} else if (isStartTag(xpp, "SegmentTimeline")) {
|
||||||
long initializationStart = Long.parseLong(indexRange[0]);
|
timeline = parseSegmentTimeline(xpp);
|
||||||
long initializationEnd = Long.parseLong(indexRange[1]);
|
|
||||||
segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd));
|
|
||||||
} else if (isStartTag(xpp, "SegmentURL")) {
|
} else if (isStartTag(xpp, "SegmentURL")) {
|
||||||
String url = xpp.getAttributeValue(null, "media");
|
if (segments == null) {
|
||||||
String mediaRange = xpp.getAttributeValue(null, "mediaRange");
|
segments = new ArrayList<RangedUri>();
|
||||||
long sequenceNumber = segmentTimelineList.get(i).sequenceNumber;
|
|
||||||
long duration = segmentTimelineList.get(i).duration;
|
|
||||||
i++;
|
|
||||||
if (mediaRange != null) {
|
|
||||||
String[] mediaRangeArray = xpp.getAttributeValue(null, "mediaRange").split("-");
|
|
||||||
long mediaStart = Long.parseLong(mediaRangeArray[0]);
|
|
||||||
segmentList.add(new Segment.Media(url, mediaStart, sequenceNumber, duration));
|
|
||||||
} else {
|
|
||||||
segmentList.add(new Segment.Media(url, sequenceNumber, duration));
|
|
||||||
}
|
}
|
||||||
|
segments.add(parseSegmentUrl(xpp, baseUrl));
|
||||||
}
|
}
|
||||||
} while (!isEndTag(xpp, "SegmentList"));
|
} while (!isEndTag(xpp, "SegmentList"));
|
||||||
|
|
||||||
return segmentList;
|
if (parent != null) {
|
||||||
|
initialization = initialization != null ? initialization : parent.initialization;
|
||||||
|
timeline = timeline != null ? timeline : parent.segmentTimeline;
|
||||||
|
segments = segments != null ? segments : parent.mediaSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration,
|
||||||
|
startNumber, duration, timeline, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl,
|
||||||
|
SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException {
|
||||||
|
|
||||||
|
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||||
|
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||||
|
parent != null ? parent.presentationTimeOffset : 0);
|
||||||
|
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
|
||||||
|
int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0);
|
||||||
|
UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media",
|
||||||
|
parent != null ? parent.mediaTemplate : null);
|
||||||
|
UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization",
|
||||||
|
parent != null ? parent.initializationTemplate : null);
|
||||||
|
|
||||||
|
RangedUri initialization = null;
|
||||||
|
List<SegmentTimelineElement> timeline = null;
|
||||||
|
|
||||||
|
do {
|
||||||
|
xpp.next();
|
||||||
|
if (isStartTag(xpp, "Initialization")) {
|
||||||
|
initialization = parseInitialization(xpp, baseUrl);
|
||||||
|
} else if (isStartTag(xpp, "SegmentTimeline")) {
|
||||||
|
timeline = parseSegmentTimeline(xpp);
|
||||||
|
}
|
||||||
|
} while (!isEndTag(xpp, "SegmentTemplate"));
|
||||||
|
|
||||||
|
if (parent != null) {
|
||||||
|
initialization = initialization != null ? initialization : parent.initialization;
|
||||||
|
timeline = timeline != null ? timeline : parent.segmentTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration,
|
||||||
|
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
|
||||||
|
throws XmlPullParserException, IOException {
|
||||||
|
List<SegmentTimelineElement> segmentTimeline = new ArrayList<SegmentTimelineElement>();
|
||||||
|
long elapsedTime = 0;
|
||||||
|
do {
|
||||||
|
xpp.next();
|
||||||
|
if (isStartTag(xpp, "S")) {
|
||||||
|
elapsedTime = parseLong(xpp, "t", elapsedTime);
|
||||||
|
long duration = parseLong(xpp, "d");
|
||||||
|
int count = 1 + parseInt(xpp, "r", 0);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
segmentTimeline.add(new SegmentTimelineElement(elapsedTime, duration));
|
||||||
|
elapsedTime += duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!isEndTag(xpp, "SegmentTimeline"));
|
||||||
|
return segmentTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name,
|
||||||
|
UrlTemplate defaultValue) {
|
||||||
|
String valueString = xpp.getAttributeValue(null, name);
|
||||||
|
if (valueString != null) {
|
||||||
|
return UrlTemplate.compile(valueString);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) {
|
||||||
|
return parseRangedUrl(xpp, baseUrl, "sourceURL", "range");
|
||||||
|
}
|
||||||
|
|
||||||
|
private RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) {
|
||||||
|
return parseRangedUrl(xpp, baseUrl, "media", "mediaRange");
|
||||||
|
}
|
||||||
|
|
||||||
|
private RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute,
|
||||||
|
String rangeAttribute) {
|
||||||
|
String urlText = xpp.getAttributeValue(null, urlAttribute);
|
||||||
|
long rangeStart = 0;
|
||||||
|
long rangeLength = -1;
|
||||||
|
String rangeText = xpp.getAttributeValue(null, rangeAttribute);
|
||||||
|
if (rangeText != null) {
|
||||||
|
String[] rangeTextArray = rangeText.split("-");
|
||||||
|
rangeStart = Long.parseLong(rangeTextArray[0]);
|
||||||
|
rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;
|
||||||
|
}
|
||||||
|
return new RangedUri(baseUrl, urlText, rangeStart, rangeLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods.
|
||||||
|
|
||||||
protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
|
protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
|
||||||
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
|
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
|
||||||
}
|
}
|
||||||
@ -313,25 +424,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
|
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static int parseInt(XmlPullParser xpp, String name) {
|
private static long parseDurationMs(XmlPullParser xpp, String name) {
|
||||||
String value = xpp.getAttributeValue(null, name);
|
|
||||||
return value == null ? -1 : Integer.parseInt(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static long parseLong(XmlPullParser xpp, String name) {
|
|
||||||
return parseLong(xpp, name, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
|
|
||||||
String value = xpp.getAttributeValue(null, name);
|
|
||||||
return value == null ? defaultValue : Long.parseLong(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseDurationMs(XmlPullParser xpp, String name) {
|
|
||||||
return parseDurationMs(xpp, name, -1);
|
return parseDurationMs(xpp, name, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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) {
|
||||||
Matcher matcher = DURATION.matcher(value);
|
Matcher matcher = DURATION.matcher(value);
|
||||||
@ -350,4 +447,38 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
|
||||||
|
throws XmlPullParserException, IOException {
|
||||||
|
xpp.next();
|
||||||
|
String newBaseUrlText = xpp.getText();
|
||||||
|
Uri newBaseUri = Uri.parse(newBaseUrlText);
|
||||||
|
if (!newBaseUri.isAbsolute()) {
|
||||||
|
newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText);
|
||||||
|
}
|
||||||
|
return newBaseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static int parseInt(XmlPullParser xpp, String name) {
|
||||||
|
return parseInt(xpp, name, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
|
||||||
|
String value = xpp.getAttributeValue(null, name);
|
||||||
|
return value == null ? defaultValue : Integer.parseInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static long parseLong(XmlPullParser xpp, String name) {
|
||||||
|
return parseLong(xpp, name, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
|
||||||
|
String value = xpp.getAttributeValue(null, name);
|
||||||
|
return value == null ? defaultValue : Long.parseLong(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {
|
||||||
|
String value = xpp.getAttributeValue(null, name);
|
||||||
|
return value == null ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,46 +23,37 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class Period {
|
public final class Period {
|
||||||
|
|
||||||
public final int id;
|
/**
|
||||||
|
* The period identifier, if one exists.
|
||||||
|
*/
|
||||||
|
public final String id;
|
||||||
|
|
||||||
public final long start;
|
/**
|
||||||
|
* The start time of the period in milliseconds.
|
||||||
|
*/
|
||||||
|
public final long startMs;
|
||||||
|
|
||||||
public final long duration;
|
/**
|
||||||
|
* The duration of the period in milliseconds, or -1 if the duration is unknown.
|
||||||
|
*/
|
||||||
|
public final long durationMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The adaptation sets belonging to the period.
|
||||||
|
*/
|
||||||
public final List<AdaptationSet> adaptationSets;
|
public final List<AdaptationSet> adaptationSets;
|
||||||
|
|
||||||
public final List<Segment.Timeline> segmentList;
|
/**
|
||||||
|
* @param id The period identifier. May be null.
|
||||||
public final int segmentStartNumber;
|
* @param start The start time of the period in milliseconds.
|
||||||
|
* @param duration The duration of the period in milliseconds, or -1 if the duration is unknown.
|
||||||
public final int segmentTimescale;
|
* @param adaptationSets The adaptation sets belonging to the period.
|
||||||
|
*/
|
||||||
public final long presentationTimeOffset;
|
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
|
||||||
|
|
||||||
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets) {
|
|
||||||
this(id, start, duration, adaptationSets, null, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
|
|
||||||
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale) {
|
|
||||||
this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Period(int id, long start, long duration, List<AdaptationSet> adaptationSets,
|
|
||||||
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale,
|
|
||||||
long presentationTimeOffset) {
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.start = start;
|
this.startMs = start;
|
||||||
this.duration = duration;
|
this.durationMs = duration;
|
||||||
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
|
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
|
||||||
if (segmentList != null) {
|
|
||||||
this.segmentList = Collections.unmodifiableList(segmentList);
|
|
||||||
} else {
|
|
||||||
this.segmentList = null;
|
|
||||||
}
|
|
||||||
this.segmentStartNumber = segmentStartNumber;
|
|
||||||
this.segmentTimescale = segmentTimescale;
|
|
||||||
this.presentationTimeOffset = presentationTimeOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a range of data located at a {@link Uri}.
|
||||||
|
*/
|
||||||
|
public final class RangedUri {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The (zero based) index of the first byte of the range.
|
||||||
|
*/
|
||||||
|
public final long start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The length of the range, or -1 to indicate that the range is unbounded.
|
||||||
|
*/
|
||||||
|
public final long length;
|
||||||
|
|
||||||
|
// The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}.
|
||||||
|
// This helps optimize memory usage in the same way that DASH manifests allow many URLs to be
|
||||||
|
// expressed concisely in the form of a single BaseURL and many relative paths. Note that this
|
||||||
|
// optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many
|
||||||
|
// instances of this class.
|
||||||
|
private final Uri baseUri;
|
||||||
|
private final String stringUri;
|
||||||
|
|
||||||
|
private int hashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an ranged uri.
|
||||||
|
* <p>
|
||||||
|
* The uri is built according to the following rules:
|
||||||
|
* <ul>
|
||||||
|
* <li>If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is
|
||||||
|
* ignored and the url consists solely of {@code stringUri}.
|
||||||
|
* <li>If {@code stringUri} is null, then the url consists solely of {@code baseUrl}.
|
||||||
|
* <li>Otherwise, the url consists of the concatenation of {@code baseUri} and {@code stringUri}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param baseUri An uri that can form the base of the uri defined by the instance.
|
||||||
|
* @param stringUri A relative or absolute uri in string form.
|
||||||
|
* @param start The (zero based) index of the first byte of the range.
|
||||||
|
* @param length The length of the range, or -1 to indicate that the range is unbounded.
|
||||||
|
*/
|
||||||
|
public RangedUri(Uri baseUri, String stringUri, long start, long length) {
|
||||||
|
Assertions.checkArgument(baseUri != null || stringUri != null);
|
||||||
|
this.baseUri = baseUri;
|
||||||
|
this.stringUri = stringUri;
|
||||||
|
this.start = start;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link Uri} represented by the instance.
|
||||||
|
*
|
||||||
|
* @return The {@link Uri} represented by the instance.
|
||||||
|
*/
|
||||||
|
public Uri getUri() {
|
||||||
|
if (stringUri == null) {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
Uri uri = Uri.parse(stringUri);
|
||||||
|
if (!uri.isAbsolute() && baseUri != null) {
|
||||||
|
uri = Uri.withAppendedPath(baseUri, stringUri);
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to merge this {@link RangedUri} with another.
|
||||||
|
* <p>
|
||||||
|
* A merge is successful if both instances define the same {@link Uri}, and if one starte the
|
||||||
|
* byte after the other ends, forming a contiguous region with no overlap.
|
||||||
|
* <p>
|
||||||
|
* If {@code other} is null then the merge is considered unsuccessful, and null is returned.
|
||||||
|
*
|
||||||
|
* @param other The {@link RangedUri} to merge.
|
||||||
|
* @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
|
||||||
|
*/
|
||||||
|
public RangedUri attemptMerge(RangedUri other) {
|
||||||
|
if (other == null || !getUri().equals(other.getUri())) {
|
||||||
|
return null;
|
||||||
|
} else if (length != -1 && start + length == other.start) {
|
||||||
|
return new RangedUri(baseUri, stringUri, start,
|
||||||
|
other.length == -1 ? -1 : length + other.length);
|
||||||
|
} else if (other.length != -1 && other.start + other.length == start) {
|
||||||
|
return new RangedUri(baseUri, stringUri, other.start,
|
||||||
|
length == -1 ? -1 : other.length + length);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
if (hashCode == 0) {
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + (int) start;
|
||||||
|
result = 31 * result + (int) length;
|
||||||
|
result = 31 * result + getUri().hashCode();
|
||||||
|
hashCode = result;
|
||||||
|
}
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RangedUri other = (RangedUri) obj;
|
||||||
|
return this.start == other.start
|
||||||
|
&& this.length == other.length
|
||||||
|
&& getUri().equals(other.getUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,13 +16,16 @@
|
|||||||
package com.google.android.exoplayer.dash.mpd;
|
package com.google.android.exoplayer.dash.mpd;
|
||||||
|
|
||||||
import com.google.android.exoplayer.chunk.Format;
|
import com.google.android.exoplayer.chunk.Format;
|
||||||
|
import com.google.android.exoplayer.dash.DashSegmentIndex;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.MultiSegmentBase;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flat version of a DASH representation.
|
* A DASH representation.
|
||||||
*/
|
*/
|
||||||
public class Representation {
|
public abstract class Representation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies the piece of content to which this {@link Representation} belongs.
|
* Identifies the piece of content to which this {@link Representation} belongs.
|
||||||
@ -33,7 +36,7 @@ public class Representation {
|
|||||||
public final String contentId;
|
public final String contentId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies the revision of the {@link Representation}.
|
* Identifies the revision of the content.
|
||||||
* <p>
|
* <p>
|
||||||
* If the media for a given ({@link #contentId} can change over time without a change to the
|
* If the media for a given ({@link #contentId} can change over time without a change to the
|
||||||
* {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
|
* {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
|
||||||
@ -43,45 +46,93 @@ public class Representation {
|
|||||||
public final long revisionId;
|
public final long revisionId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The format in which the {@link Representation} is encoded.
|
* The format of the representation.
|
||||||
*/
|
*/
|
||||||
public final Format format;
|
public final Format format;
|
||||||
|
|
||||||
public final long contentLength;
|
/**
|
||||||
|
* The start time of the enclosing period in milliseconds since the epoch.
|
||||||
|
*/
|
||||||
|
public final long periodStartMs;
|
||||||
|
|
||||||
public final long initializationStart;
|
/**
|
||||||
|
* The duration of the enclosing period in milliseconds.
|
||||||
|
*/
|
||||||
|
public final long periodDurationMs;
|
||||||
|
|
||||||
public final long initializationEnd;
|
/**
|
||||||
|
* The offset of the presentation timestamps in the media stream relative to media time.
|
||||||
|
*/
|
||||||
|
public final long presentationTimeOffsetMs;
|
||||||
|
|
||||||
public final long indexStart;
|
private final RangedUri initializationUri;
|
||||||
|
|
||||||
public final long indexEnd;
|
/**
|
||||||
|
* Constructs a new instance.
|
||||||
|
*
|
||||||
|
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||||
|
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||||
|
* duration is unknown.
|
||||||
|
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||||
|
* @param revisionId Identifies the revision of the content.
|
||||||
|
* @param format The format of the representation.
|
||||||
|
* @param segmentBase A segment base element for the representation.
|
||||||
|
* @return The constructed instance.
|
||||||
|
*/
|
||||||
|
public static Representation newInstance(long periodStartMs, long periodDurationMs,
|
||||||
|
String contentId, long revisionId, Format format, SegmentBase segmentBase) {
|
||||||
|
if (segmentBase instanceof SingleSegmentBase) {
|
||||||
|
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||||
|
format, (SingleSegmentBase) segmentBase, -1);
|
||||||
|
} else if (segmentBase instanceof MultiSegmentBase) {
|
||||||
|
return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||||
|
format, (MultiSegmentBase) segmentBase);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
|
||||||
|
+ "MultiSegmentBase");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final long periodStart;
|
private Representation(long periodStartMs, long periodDurationMs, String contentId,
|
||||||
|
long revisionId, Format format, SegmentBase segmentBase) {
|
||||||
public final long periodDuration;
|
this.periodStartMs = periodStartMs;
|
||||||
|
this.periodDurationMs = periodDurationMs;
|
||||||
public final Uri uri;
|
|
||||||
|
|
||||||
public Representation(String contentId, long revisionId, Format format, Uri uri,
|
|
||||||
long contentLength, long initializationStart, long initializationEnd, long indexStart,
|
|
||||||
long indexEnd, long periodStart, long periodDuration) {
|
|
||||||
this.contentId = contentId;
|
this.contentId = contentId;
|
||||||
this.revisionId = revisionId;
|
this.revisionId = revisionId;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
this.contentLength = contentLength;
|
initializationUri = segmentBase.getInitialization(this);
|
||||||
this.initializationStart = initializationStart;
|
presentationTimeOffsetMs = (segmentBase.presentationTimeOffset * 1000) / segmentBase.timescale;
|
||||||
this.initializationEnd = initializationEnd;
|
|
||||||
this.indexStart = indexStart;
|
|
||||||
this.indexEnd = indexEnd;
|
|
||||||
this.periodStart = periodStart;
|
|
||||||
this.periodDuration = periodDuration;
|
|
||||||
this.uri = uri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link RangedUri} defining the location of the representation's initialization data.
|
||||||
|
* May be null if no initialization data exists.
|
||||||
|
*
|
||||||
|
* @return A {@link RangedUri} defining the location of the initialization data, or null.
|
||||||
|
*/
|
||||||
|
public RangedUri getInitializationUri() {
|
||||||
|
return initializationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link RangedUri} defining the location of the representation's segment index. Null if
|
||||||
|
* the representation provides an index directly.
|
||||||
|
*
|
||||||
|
* @return The location of the segment index, or null.
|
||||||
|
*/
|
||||||
|
public abstract RangedUri getIndexUri();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a segment index, if the representation is able to provide one directly. Null if the
|
||||||
|
* segment index is defined externally.
|
||||||
|
*
|
||||||
|
* @return The segment index, or null.
|
||||||
|
*/
|
||||||
|
public abstract DashSegmentIndex getIndex();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a cache key for the {@link Representation}, in the format
|
* Generates a cache key for the {@link Representation}, in the format
|
||||||
* {@link #contentId}.{@link #format.id}.{@link #revisionId}.
|
* {@code contentId + "." + format.id + "." + revisionId}.
|
||||||
*
|
*
|
||||||
* @return A cache key.
|
* @return A cache key.
|
||||||
*/
|
*/
|
||||||
@ -89,4 +140,143 @@ public class Representation {
|
|||||||
return contentId + "." + format.id + "." + revisionId;
|
return contentId + "." + format.id + "." + revisionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DASH representation consisting of a single segment.
|
||||||
|
*/
|
||||||
|
public static class SingleSegmentRepresentation extends Representation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Uri} of the single segment.
|
||||||
|
*/
|
||||||
|
public final Uri uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content length, or -1 if unknown.
|
||||||
|
*/
|
||||||
|
public final long contentLength;
|
||||||
|
|
||||||
|
private final RangedUri indexUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||||
|
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||||
|
* duration is unknown.
|
||||||
|
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||||
|
* @param revisionId Identifies the revision of the content.
|
||||||
|
* @param format The format of the representation.
|
||||||
|
* @param uri The uri of the media.
|
||||||
|
* @param initializationStart The offset of the first byte of initialization data.
|
||||||
|
* @param initializationEnd The offset of the last byte of initialization data.
|
||||||
|
* @param indexStart The offset of the first byte of index data.
|
||||||
|
* @param indexEnd The offset of the last byte of index data.
|
||||||
|
* @param contentLength The content length, or -1 if unknown.
|
||||||
|
*/
|
||||||
|
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
|
||||||
|
String contentId, long revisionId, Format format, Uri uri, long initializationStart,
|
||||||
|
long initializationEnd, long indexStart, long indexEnd, long contentLength) {
|
||||||
|
RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
|
||||||
|
initializationEnd - initializationStart + 1);
|
||||||
|
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
|
||||||
|
indexEnd - indexStart + 1);
|
||||||
|
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||||
|
format, segmentBase, contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||||
|
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||||
|
* duration is unknown.
|
||||||
|
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||||
|
* @param revisionId Identifies the revision of the content.
|
||||||
|
* @param format The format of the representation.
|
||||||
|
* @param segmentBase The segment base underlying the representation.
|
||||||
|
* @param contentLength The content length, or -1 if unknown.
|
||||||
|
*/
|
||||||
|
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
|
||||||
|
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
|
||||||
|
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
|
||||||
|
this.uri = segmentBase.uri;
|
||||||
|
this.indexUri = segmentBase.getIndex();
|
||||||
|
this.contentLength = contentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RangedUri getIndexUri() {
|
||||||
|
return indexUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DashSegmentIndex getIndex() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DASH representation consisting of multiple segments.
|
||||||
|
*/
|
||||||
|
public static class MultiSegmentRepresentation extends Representation
|
||||||
|
implements DashSegmentIndex {
|
||||||
|
|
||||||
|
private final MultiSegmentBase segmentBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||||
|
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||||
|
* duration is unknown.
|
||||||
|
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||||
|
* @param revisionId Identifies the revision of the content.
|
||||||
|
* @param format The format of the representation.
|
||||||
|
* @param segmentBase The segment base underlying the representation.
|
||||||
|
*/
|
||||||
|
public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
|
||||||
|
long revisionId, Format format, MultiSegmentBase segmentBase) {
|
||||||
|
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
|
||||||
|
this.segmentBase = segmentBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RangedUri getIndexUri() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DashSegmentIndex getIndex() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashSegmentIndex implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RangedUri getSegmentUrl(int segmentIndex) {
|
||||||
|
return segmentBase.getSegmentUrl(this, segmentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSegmentNum(long timeUs) {
|
||||||
|
return segmentBase.getSegmentNum(timeUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getTimeUs(int segmentIndex) {
|
||||||
|
return segmentBase.getSegmentTimeUs(segmentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getDurationUs(int segmentIndex) {
|
||||||
|
return segmentBase.getSegmentDurationUs(segmentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getFirstSegmentNum() {
|
||||||
|
return segmentBase.getFirstSegmentNum();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLastSegmentNum() {
|
||||||
|
return segmentBase.getLastSegmentNum();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,81 +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.mpd;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a particular segment in a Representation.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public abstract class Segment {
|
|
||||||
|
|
||||||
public final String relativeUri;
|
|
||||||
|
|
||||||
public final long sequenceNumber;
|
|
||||||
|
|
||||||
public final long duration;
|
|
||||||
|
|
||||||
public Segment(String relativeUri, long sequenceNumber, long duration) {
|
|
||||||
this.relativeUri = relativeUri;
|
|
||||||
this.sequenceNumber = sequenceNumber;
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a timeline segment from the MPD's SegmentTimeline list.
|
|
||||||
*/
|
|
||||||
public static class Timeline extends Segment {
|
|
||||||
|
|
||||||
public Timeline(long sequenceNumber, long duration) {
|
|
||||||
super(null, sequenceNumber, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an initialization segment.
|
|
||||||
*/
|
|
||||||
public static class Initialization extends Segment {
|
|
||||||
|
|
||||||
public final long initializationStart;
|
|
||||||
public final long initializationEnd;
|
|
||||||
|
|
||||||
public Initialization(String relativeUri, long initializationStart,
|
|
||||||
long initializationEnd) {
|
|
||||||
super(relativeUri, -1, -1);
|
|
||||||
this.initializationStart = initializationStart;
|
|
||||||
this.initializationEnd = initializationEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a media segment.
|
|
||||||
*/
|
|
||||||
public static class Media extends Segment {
|
|
||||||
|
|
||||||
public final long mediaStart;
|
|
||||||
|
|
||||||
public Media(String relativeUri, long sequenceNumber, long duration) {
|
|
||||||
this(relativeUri, 0, sequenceNumber, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Media(String uri, long mediaStart, long sequenceNumber, long duration) {
|
|
||||||
super(uri, sequenceNumber, duration);
|
|
||||||
this.mediaStart = mediaStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +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.mpd;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer.chunk.Format;
|
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a DASH Representation which uses the SegmentList structure (i.e. it has a list of
|
|
||||||
* Segment URLs instead of a single URL).
|
|
||||||
*/
|
|
||||||
public class SegmentedRepresentation extends Representation {
|
|
||||||
|
|
||||||
private List<Segment> segmentList;
|
|
||||||
|
|
||||||
public SegmentedRepresentation(String contentId, Format format, Uri uri, long initializationStart,
|
|
||||||
long initializationEnd, long indexStart, long indexEnd, long periodStart, long periodDuration,
|
|
||||||
List<Segment> segmentList) {
|
|
||||||
super(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, initializationStart,
|
|
||||||
initializationEnd, indexStart, indexEnd, periodStart, periodDuration);
|
|
||||||
this.segmentList = segmentList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNumSegments() {
|
|
||||||
return segmentList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Segment getSegment(int i) {
|
|
||||||
return segmentList.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A template from which URLs can be built.
|
||||||
|
* <p>
|
||||||
|
* URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4.
|
||||||
|
*/
|
||||||
|
public final class UrlTemplate {
|
||||||
|
|
||||||
|
private static final String REPRESENTATION = "RepresentationID";
|
||||||
|
private static final String NUMBER = "Number";
|
||||||
|
private static final String BANDWIDTH = "Bandwidth";
|
||||||
|
private static final String TIME = "Time";
|
||||||
|
private static final String ESCAPED_DOLLAR = "$$";
|
||||||
|
private static final String DEFAULT_FORMAT_TAG = "%01d";
|
||||||
|
|
||||||
|
private static final int REPRESENTATION_ID = 1;
|
||||||
|
private static final int NUMBER_ID = 2;
|
||||||
|
private static final int BANDWIDTH_ID = 3;
|
||||||
|
private static final int TIME_ID = 4;
|
||||||
|
|
||||||
|
private final String[] urlPieces;
|
||||||
|
private final int[] identifiers;
|
||||||
|
private final String[] identifierFormatTags;
|
||||||
|
private final int identifierCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile an instance from the provided template string.
|
||||||
|
*
|
||||||
|
* @param template The template.
|
||||||
|
* @return The compiled instance.
|
||||||
|
* @throws IllegalArgumentException If the template string is malformed.
|
||||||
|
*/
|
||||||
|
public static UrlTemplate compile(String template) {
|
||||||
|
// These arrays are sizes assuming each of the four possible identifiers will be present at
|
||||||
|
// most once in the template, which seems like a reasonable assumption.
|
||||||
|
String[] urlPieces = new String[5];
|
||||||
|
int[] identifiers = new int[4];
|
||||||
|
String[] identifierFormatTags = new String[4];
|
||||||
|
int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags);
|
||||||
|
return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal constructor. Use {@link #compile(String)} to build instances of this class.
|
||||||
|
*/
|
||||||
|
private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags,
|
||||||
|
int identifierCount) {
|
||||||
|
this.urlPieces = urlPieces;
|
||||||
|
this.identifiers = identifiers;
|
||||||
|
this.identifierFormatTags = identifierFormatTags;
|
||||||
|
this.identifierCount = identifierCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a Uri from the template, substituting in the provided arguments.
|
||||||
|
* <p>
|
||||||
|
* Arguments whose corresponding identifiers are not present in the template will be ignored.
|
||||||
|
*
|
||||||
|
* @param representationId The representation identifier.
|
||||||
|
* @param segmentNumber The segment number.
|
||||||
|
* @param bandwidth The bandwidth.
|
||||||
|
* @param time The time as specified by the segment timeline.
|
||||||
|
* @return The built Uri.
|
||||||
|
*/
|
||||||
|
public String buildUri(String representationId, int segmentNumber, int bandwidth, long time) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < identifierCount; i++) {
|
||||||
|
builder.append(urlPieces[i]);
|
||||||
|
if (identifiers[i] == REPRESENTATION_ID) {
|
||||||
|
builder.append(representationId);
|
||||||
|
} else if (identifiers[i] == NUMBER_ID) {
|
||||||
|
builder.append(String.format(identifierFormatTags[i], segmentNumber));
|
||||||
|
} else if (identifiers[i] == BANDWIDTH_ID) {
|
||||||
|
builder.append(String.format(identifierFormatTags[i], bandwidth));
|
||||||
|
} else if (identifiers[i] == TIME_ID) {
|
||||||
|
builder.append(String.format(identifierFormatTags[i], time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.append(urlPieces[identifierCount]);
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses {@code template}, placing the decomposed components into the provided arrays.
|
||||||
|
* <p>
|
||||||
|
* If the return value is N, {@code urlPieces} will contain (N+1) strings that must be
|
||||||
|
* interleaved with N arguments in order to construct a url. The N identifiers that correspond to
|
||||||
|
* the required arguments, together with the tags that define their required formatting, are
|
||||||
|
* returned in {@code identifiers} and {@code identifierFormatTags} respectively.
|
||||||
|
*
|
||||||
|
* @param template The template to parse.
|
||||||
|
* @param urlPieces A holder for pieces of url parsed from the template.
|
||||||
|
* @param identifiers A holder for identifiers parsed from the template.
|
||||||
|
* @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers.
|
||||||
|
* @return The number of identifiers in the template url.
|
||||||
|
* @throws IllegalArgumentException If the template string is malformed.
|
||||||
|
*/
|
||||||
|
private static int parseTemplate(String template, String[] urlPieces, int[] identifiers,
|
||||||
|
String[] identifierFormatTags) {
|
||||||
|
urlPieces[0] = "";
|
||||||
|
int templateIndex = 0;
|
||||||
|
int identifierCount = 0;
|
||||||
|
while (templateIndex < template.length()) {
|
||||||
|
int dollarIndex = template.indexOf("$", templateIndex);
|
||||||
|
if (dollarIndex == -1) {
|
||||||
|
urlPieces[identifierCount] += template.substring(templateIndex);
|
||||||
|
templateIndex = template.length();
|
||||||
|
} else if (dollarIndex != templateIndex) {
|
||||||
|
urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex);
|
||||||
|
templateIndex = dollarIndex;
|
||||||
|
} else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) {
|
||||||
|
urlPieces[identifierCount] += "$";
|
||||||
|
templateIndex += 2;
|
||||||
|
} else {
|
||||||
|
int secondIndex = template.indexOf("$", templateIndex + 1);
|
||||||
|
String identifier = template.substring(templateIndex + 1, secondIndex);
|
||||||
|
if (identifier.equals(REPRESENTATION)) {
|
||||||
|
identifiers[identifierCount] = REPRESENTATION_ID;
|
||||||
|
} else {
|
||||||
|
int formatTagIndex = identifier.indexOf("%0");
|
||||||
|
String formatTag = DEFAULT_FORMAT_TAG;
|
||||||
|
if (formatTagIndex != -1) {
|
||||||
|
formatTag = identifier.substring(formatTagIndex);
|
||||||
|
if (!formatTag.endsWith("d")) {
|
||||||
|
formatTag += "d";
|
||||||
|
}
|
||||||
|
identifier = identifier.substring(0, formatTagIndex);
|
||||||
|
}
|
||||||
|
if (identifier.equals(NUMBER)) {
|
||||||
|
identifiers[identifierCount] = NUMBER_ID;
|
||||||
|
} else if (identifier.equals(BANDWIDTH)) {
|
||||||
|
identifiers[identifierCount] = BANDWIDTH_ID;
|
||||||
|
} else if (identifier.equals(TIME)) {
|
||||||
|
identifiers[identifierCount] = TIME_ID;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid template: " + template);
|
||||||
|
}
|
||||||
|
identifierFormatTags[identifierCount] = formatTag;
|
||||||
|
}
|
||||||
|
identifierCount++;
|
||||||
|
urlPieces[identifierCount] = "";
|
||||||
|
templateIndex = secondIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return identifierCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -21,6 +21,7 @@ import java.util.List;
|
|||||||
/* package */ abstract class Atom {
|
/* package */ abstract class Atom {
|
||||||
|
|
||||||
public static final int TYPE_avc1 = 0x61766331;
|
public static final int TYPE_avc1 = 0x61766331;
|
||||||
|
public static final int TYPE_avc3 = 0x61766333;
|
||||||
public static final int TYPE_esds = 0x65736473;
|
public static final int TYPE_esds = 0x65736473;
|
||||||
public static final int TYPE_mdat = 0x6D646174;
|
public static final int TYPE_mdat = 0x6D646174;
|
||||||
public static final int TYPE_mfhd = 0x6D666864;
|
public static final int TYPE_mfhd = 0x6D666864;
|
||||||
|
@ -49,6 +49,15 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
public final class FragmentedMp4Extractor {
|
public final class FragmentedMp4Extractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
|
||||||
|
* The workaround overrides the sync frame flags in the stream, forcing them to false except for
|
||||||
|
* the first sample in each segment.
|
||||||
|
* <p>
|
||||||
|
* This flag does nothing if the stream is not a video stream.
|
||||||
|
*/
|
||||||
|
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An attempt to read from the input stream returned 0 bytes of data.
|
* An attempt to read from the input stream returned 0 bytes of data.
|
||||||
*/
|
*/
|
||||||
@ -74,9 +83,13 @@ public final class FragmentedMp4Extractor {
|
|||||||
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
|
* A sidx atom was read. The parsed data can be read using {@link #getSegmentIndex()}.
|
||||||
*/
|
*/
|
||||||
public static final int RESULT_READ_SIDX = 32;
|
public static final int RESULT_READ_SIDX = 32;
|
||||||
|
/**
|
||||||
|
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
|
||||||
|
*/
|
||||||
|
public static final int RESULT_NEED_SAMPLE_HOLDER = 64;
|
||||||
|
|
||||||
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_FULL;
|
| RESULT_READ_SAMPLE_FULL | 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};
|
||||||
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
|
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
|
||||||
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
|
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
|
||||||
@ -97,6 +110,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
static {
|
static {
|
||||||
HashSet<Integer> parsedAtoms = new HashSet<Integer>();
|
HashSet<Integer> parsedAtoms = new HashSet<Integer>();
|
||||||
parsedAtoms.add(Atom.TYPE_avc1);
|
parsedAtoms.add(Atom.TYPE_avc1);
|
||||||
|
parsedAtoms.add(Atom.TYPE_avc3);
|
||||||
parsedAtoms.add(Atom.TYPE_esds);
|
parsedAtoms.add(Atom.TYPE_esds);
|
||||||
parsedAtoms.add(Atom.TYPE_hdlr);
|
parsedAtoms.add(Atom.TYPE_hdlr);
|
||||||
parsedAtoms.add(Atom.TYPE_mdat);
|
parsedAtoms.add(Atom.TYPE_mdat);
|
||||||
@ -140,7 +154,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
|
CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final boolean enableSmoothStreamingWorkarounds;
|
private final int workaroundFlags;
|
||||||
|
|
||||||
// Parser state
|
// Parser state
|
||||||
private final ParsableByteArray atomHeader;
|
private final ParsableByteArray atomHeader;
|
||||||
@ -172,16 +186,15 @@ public final class FragmentedMp4Extractor {
|
|||||||
private TrackFragment fragmentRun;
|
private TrackFragment fragmentRun;
|
||||||
|
|
||||||
public FragmentedMp4Extractor() {
|
public FragmentedMp4Extractor() {
|
||||||
this(false);
|
this(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param enableSmoothStreamingWorkarounds Set to true if this extractor will be used to parse
|
* @param workaroundFlags Flags to allow parsing of faulty streams.
|
||||||
* SmoothStreaming streams. This will enable workarounds for SmoothStreaming violations of
|
* {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
|
||||||
* the ISO base media file format (ISO 14496-12). Set to false otherwise.
|
|
||||||
*/
|
*/
|
||||||
public FragmentedMp4Extractor(boolean enableSmoothStreamingWorkarounds) {
|
public FragmentedMp4Extractor(int workaroundFlags) {
|
||||||
this.enableSmoothStreamingWorkarounds = enableSmoothStreamingWorkarounds;
|
this.workaroundFlags = workaroundFlags;
|
||||||
parserState = STATE_READING_ATOM_HEADER;
|
parserState = STATE_READING_ATOM_HEADER;
|
||||||
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
|
atomHeader = new ParsableByteArray(ATOM_HEADER_SIZE);
|
||||||
containerAtoms = new Stack<ContainerAtom>();
|
containerAtoms = new Stack<ContainerAtom>();
|
||||||
@ -263,7 +276,8 @@ public final class FragmentedMp4Extractor {
|
|||||||
* in subsequent calls until the whole sample has been read.
|
* in subsequent calls until the whole sample has been read.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @param inputStream The input stream from which data should be read.
|
||||||
* @param out A {@link SampleHolder} into which the sample 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.
|
* @return One or more of the {@code RESULT_*} flags defined in this class.
|
||||||
* @throws ParserException If an error occurs parsing the media data.
|
* @throws ParserException If an error occurs parsing the media data.
|
||||||
*/
|
*/
|
||||||
@ -466,7 +480,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
|
|
||||||
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
private void onMoofContainerAtomRead(ContainerAtom moof) {
|
||||||
fragmentRun = new TrackFragment();
|
fragmentRun = new TrackFragment();
|
||||||
parseMoof(track, extendsDefaults, moof, fragmentRun, enableSmoothStreamingWorkarounds);
|
parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags);
|
||||||
sampleIndex = 0;
|
sampleIndex = 0;
|
||||||
lastSyncSampleIndex = 0;
|
lastSyncSampleIndex = 0;
|
||||||
pendingSeekSyncSampleIndex = 0;
|
pendingSeekSyncSampleIndex = 0;
|
||||||
@ -572,11 +586,12 @@ public final class FragmentedMp4Extractor {
|
|||||||
int childStartPosition = stsd.getPosition();
|
int childStartPosition = stsd.getPosition();
|
||||||
int childAtomSize = stsd.readInt();
|
int childAtomSize = stsd.readInt();
|
||||||
int childAtomType = stsd.readInt();
|
int childAtomType = stsd.readInt();
|
||||||
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_encv) {
|
if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3
|
||||||
Pair<MediaFormat, TrackEncryptionBox> avc1 =
|
|| childAtomType == Atom.TYPE_encv) {
|
||||||
parseAvc1FromParent(stsd, childStartPosition, childAtomSize);
|
Pair<MediaFormat, TrackEncryptionBox> avc =
|
||||||
mediaFormat = avc1.first;
|
parseAvcFromParent(stsd, childStartPosition, childAtomSize);
|
||||||
trackEncryptionBoxes[i] = avc1.second;
|
mediaFormat = avc.first;
|
||||||
|
trackEncryptionBoxes[i] = avc.second;
|
||||||
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) {
|
} else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca) {
|
||||||
Pair<MediaFormat, TrackEncryptionBox> mp4a =
|
Pair<MediaFormat, TrackEncryptionBox> mp4a =
|
||||||
parseMp4aFromParent(stsd, childStartPosition, childAtomSize);
|
parseMp4aFromParent(stsd, childStartPosition, childAtomSize);
|
||||||
@ -588,7 +603,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
return Pair.create(mediaFormat, trackEncryptionBoxes);
|
return Pair.create(mediaFormat, trackEncryptionBoxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pair<MediaFormat, TrackEncryptionBox> parseAvc1FromParent(ParsableByteArray parent,
|
private static Pair<MediaFormat, TrackEncryptionBox> parseAvcFromParent(ParsableByteArray parent,
|
||||||
int position, int size) {
|
int position, int size) {
|
||||||
parent.setPosition(position + ATOM_HEADER_SIZE);
|
parent.setPosition(position + ATOM_HEADER_SIZE);
|
||||||
|
|
||||||
@ -695,7 +710,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
int childAtomSize = parent.readInt();
|
int childAtomSize = parent.readInt();
|
||||||
int childAtomType = parent.readInt();
|
int childAtomType = parent.readInt();
|
||||||
if (childAtomType == Atom.TYPE_frma) {
|
if (childAtomType == Atom.TYPE_frma) {
|
||||||
parent.readInt(); // dataFormat. Expect TYPE_avc1 (video) or TYPE_mp4a (audio).
|
parent.readInt(); // dataFormat.
|
||||||
} else if (childAtomType == Atom.TYPE_schm) {
|
} else if (childAtomType == Atom.TYPE_schm) {
|
||||||
parent.skip(4);
|
parent.skip(4);
|
||||||
parent.readInt(); // schemeType. Expect cenc
|
parent.readInt(); // schemeType. Expect cenc
|
||||||
@ -774,11 +789,11 @@ public final class FragmentedMp4Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
|
private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
|
||||||
ContainerAtom moof, TrackFragment out, boolean enableSmoothStreamingWorkarounds) {
|
ContainerAtom moof, TrackFragment out, int workaroundFlags) {
|
||||||
// TODO: Consider checking that the sequence number returned by parseMfhd is as expected.
|
// TODO: Consider checking that the sequence number returned by parseMfhd is as expected.
|
||||||
parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
|
parseMfhd(moof.getLeafAtomOfType(Atom.TYPE_mfhd).getData());
|
||||||
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
|
parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
|
||||||
out, enableSmoothStreamingWorkarounds);
|
out, workaroundFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -796,7 +811,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
* Parses a traf atom (defined in 14496-12).
|
* Parses a traf atom (defined in 14496-12).
|
||||||
*/
|
*/
|
||||||
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
|
private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
|
||||||
ContainerAtom traf, TrackFragment out, boolean enableSmoothStreamingWorkarounds) {
|
ContainerAtom traf, TrackFragment out, int workaroundFlags) {
|
||||||
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
|
||||||
if (saiz != null) {
|
if (saiz != null) {
|
||||||
parseSaiz(saiz.getData(), out);
|
parseSaiz(saiz.getData(), out);
|
||||||
@ -809,8 +824,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
|
out.setSampleDescriptionIndex(fragmentHeader.sampleDescriptionIndex);
|
||||||
|
|
||||||
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
|
||||||
parseTrun(track, fragmentHeader, decodeTime, enableSmoothStreamingWorkarounds, trun.getData(),
|
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.getData(), out);
|
||||||
out);
|
|
||||||
LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
|
LeafAtom uuid = traf.getLeafAtomOfType(Atom.TYPE_uuid);
|
||||||
if (uuid != null) {
|
if (uuid != null) {
|
||||||
parseUuid(uuid.getData(), out);
|
parseUuid(uuid.getData(), out);
|
||||||
@ -895,8 +909,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
* @param out The {@TrackFragment} into which parsed data should be placed.
|
* @param out The {@TrackFragment} into which parsed data should be placed.
|
||||||
*/
|
*/
|
||||||
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
|
private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
|
||||||
long decodeTime, boolean enableSmoothStreamingWorkarounds, ParsableByteArray trun,
|
long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
|
||||||
TrackFragment out) {
|
|
||||||
trun.setPosition(ATOM_HEADER_SIZE);
|
trun.setPosition(ATOM_HEADER_SIZE);
|
||||||
int fullAtom = trun.readInt();
|
int fullAtom = trun.readInt();
|
||||||
int version = parseFullAtomVersion(fullAtom);
|
int version = parseFullAtomVersion(fullAtom);
|
||||||
@ -926,6 +939,9 @@ public final class FragmentedMp4Extractor {
|
|||||||
|
|
||||||
long timescale = track.timescale;
|
long timescale = track.timescale;
|
||||||
long cumulativeTime = decodeTime;
|
long cumulativeTime = decodeTime;
|
||||||
|
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
|
||||||
|
&& ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
|
||||||
|
== WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
|
||||||
for (int i = 0; i < numberOfEntries; i++) {
|
for (int i = 0; i < numberOfEntries; i++) {
|
||||||
// Use trun values if present, otherwise tfhd, otherwise trex.
|
// Use trun values if present, otherwise tfhd, otherwise trex.
|
||||||
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
|
int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
|
||||||
@ -934,11 +950,14 @@ public final class FragmentedMp4Extractor {
|
|||||||
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
|
int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
|
||||||
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
|
: sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
|
||||||
if (sampleCompositionTimeOffsetsPresent) {
|
if (sampleCompositionTimeOffsetsPresent) {
|
||||||
// Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by specifying
|
|
||||||
// the sample offset as a signed integer in conjunction with a box version of 0.
|
|
||||||
int sampleOffset;
|
int sampleOffset;
|
||||||
if (version == 0 && !enableSmoothStreamingWorkarounds) {
|
if (version == 0) {
|
||||||
sampleOffset = trun.readUnsignedIntToInt();
|
// The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
|
||||||
|
// version 0 trun boxes, however a significant number of streams violate the spec and use
|
||||||
|
// signed integers instead. It's safe to always parse sample offsets as signed integers
|
||||||
|
// here, because unsigned integers will still be parsed correctly (unless their top bit is
|
||||||
|
// set, which is never true in practice because sample offsets are always small).
|
||||||
|
sampleOffset = trun.readInt();
|
||||||
} else {
|
} else {
|
||||||
sampleOffset = trun.readInt();
|
sampleOffset = trun.readInt();
|
||||||
}
|
}
|
||||||
@ -947,9 +966,7 @@ public final class FragmentedMp4Extractor {
|
|||||||
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
|
sampleDecodingTimeTable[i] = (int) ((cumulativeTime * 1000) / timescale);
|
||||||
sampleSizeTable[i] = sampleSize;
|
sampleSizeTable[i] = sampleSize;
|
||||||
boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
|
boolean isSync = ((sampleFlags >> 16) & 0x1) == 0;
|
||||||
if (track.type == Track.TYPE_VIDEO && enableSmoothStreamingWorkarounds && i != 0) {
|
if (workaroundEveryVideoFrameIsSyncFrame && i != 0) {
|
||||||
// Fragmented mp4 streams packaged for smooth streaming violate the BMFF spec by indicating
|
|
||||||
// that every sample is a sync frame, when this is not actually the case.
|
|
||||||
isSync = false;
|
isSync = false;
|
||||||
}
|
}
|
||||||
if (isSync) {
|
if (isSync) {
|
||||||
@ -1130,6 +1147,9 @@ public final class FragmentedMp4Extractor {
|
|||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
|
private int readSample(NonBlockingInputStream inputStream, SampleHolder out) {
|
||||||
|
if (out == null) {
|
||||||
|
return RESULT_NEED_SAMPLE_HOLDER;
|
||||||
|
}
|
||||||
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
|
||||||
ByteBuffer outputData = out.data;
|
ByteBuffer outputData = out.data;
|
||||||
if (parserState == STATE_READING_SAMPLE_START) {
|
if (parserState == STATE_READING_SAMPLE_START) {
|
||||||
|
@ -0,0 +1,558 @@
|
|||||||
|
/*
|
||||||
|
* 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.upstream.NonBlockingInputStream;
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Stack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default version of a basic event-driven incremental EBML parser which needs an
|
||||||
|
* {@link EbmlEventHandler} to define IDs/types and react to events.
|
||||||
|
*
|
||||||
|
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
|
||||||
|
* It was originally designed for the Matroska container format. More information about EBML and
|
||||||
|
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
||||||
|
*/
|
||||||
|
/* package */ final class DefaultEbmlReader implements EbmlReader {
|
||||||
|
|
||||||
|
// State values used in variables state, elementIdState, elementContentSizeState, and
|
||||||
|
// varintBytesState.
|
||||||
|
private static final int STATE_BEGIN_READING = 0;
|
||||||
|
private static final int STATE_READ_CONTENTS = 1;
|
||||||
|
private static final int STATE_FINISHED_READING = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first byte of a variable-length integer (varint) will have one of these bit masks
|
||||||
|
* indicating the total length in bytes.
|
||||||
|
*
|
||||||
|
* <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
|
||||||
|
*/
|
||||||
|
private static final int[] VARINT_LENGTH_MASKS = new int[] {
|
||||||
|
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
|
||||||
|
private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
|
||||||
|
private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scratch space to read in EBML varints, unsigned ints, and floats - each of which can be
|
||||||
|
* up to 8 bytes.
|
||||||
|
*/
|
||||||
|
private final byte[] tempByteArray = new byte[8];
|
||||||
|
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current {@link EbmlEventHandler} which is queried for element types
|
||||||
|
* and informed of element events.
|
||||||
|
*/
|
||||||
|
private EbmlEventHandler eventHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overall state for the current element. Must be one of the {@code STATE_*} constants.
|
||||||
|
*/
|
||||||
|
private int state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total bytes read since starting or the last {@link #reset()}.
|
||||||
|
*/
|
||||||
|
private long bytesRead;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The starting byte offset of the current element being parsed.
|
||||||
|
*/
|
||||||
|
private long elementOffset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current element ID after {@link #elementIdState} is {@link #STATE_FINISHED_READING}.
|
||||||
|
*/
|
||||||
|
private int elementId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for the ID of the current element. Must be one of the {@code STATE_*} constants.
|
||||||
|
*/
|
||||||
|
private int elementIdState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current element content size after {@link #elementContentSizeState}
|
||||||
|
* is {@link #STATE_FINISHED_READING}.
|
||||||
|
*/
|
||||||
|
private long elementContentSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for the content size of the current element.
|
||||||
|
* Must be one of the {@code STATE_*} constants.
|
||||||
|
*/
|
||||||
|
private int elementContentSizeState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for the current variable-length integer (varint) being read into
|
||||||
|
* {@link #tempByteArray}. Must be one of the {@code STATE_*} constants.
|
||||||
|
*/
|
||||||
|
private int varintBytesState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Length in bytes of the current variable-length integer (varint) being read into
|
||||||
|
* {@link #tempByteArray}.
|
||||||
|
*/
|
||||||
|
private int varintBytesLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of bytes being contiguously read into either {@link #tempByteArray} or
|
||||||
|
* {@link #stringBytes}. Used to determine when all required bytes have been read across
|
||||||
|
* multiple calls.
|
||||||
|
*/
|
||||||
|
private int bytesState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds string element bytes as they're being read in. Allocated after the element content
|
||||||
|
* size is known and released after calling {@link EbmlEventHandler#onStringElement(int, String)}.
|
||||||
|
*/
|
||||||
|
private byte[] stringBytes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEventHandler(EbmlEventHandler eventHandler) {
|
||||||
|
this.eventHandler = eventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(NonBlockingInputStream inputStream) {
|
||||||
|
Assertions.checkState(eventHandler != null);
|
||||||
|
while (true) {
|
||||||
|
while (!masterElementsStack.isEmpty()
|
||||||
|
&& bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
|
||||||
|
if (!eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId)) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == STATE_BEGIN_READING) {
|
||||||
|
int idResult = readElementId(inputStream);
|
||||||
|
if (idResult != READ_RESULT_CONTINUE) {
|
||||||
|
return idResult;
|
||||||
|
}
|
||||||
|
int sizeResult = readElementContentSize(inputStream);
|
||||||
|
if (sizeResult != READ_RESULT_CONTINUE) {
|
||||||
|
return sizeResult;
|
||||||
|
}
|
||||||
|
state = STATE_READ_CONTENTS;
|
||||||
|
bytesState = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int type = eventHandler.getElementType(elementId);
|
||||||
|
switch (type) {
|
||||||
|
case TYPE_MASTER:
|
||||||
|
int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
|
||||||
|
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
|
||||||
|
if (!eventHandler.onMasterElementStart(
|
||||||
|
elementId, elementOffset, masterHeaderSize, elementContentSize)) {
|
||||||
|
prepareForNextElement();
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TYPE_UNSIGNED_INT:
|
||||||
|
if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
|
||||||
|
throw new IllegalStateException("Invalid integer size " + elementContentSize);
|
||||||
|
}
|
||||||
|
int intResult =
|
||||||
|
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
|
||||||
|
if (intResult != READ_RESULT_CONTINUE) {
|
||||||
|
return intResult;
|
||||||
|
}
|
||||||
|
long intValue = getTempByteArrayValue((int) elementContentSize, false);
|
||||||
|
if (!eventHandler.onIntegerElement(elementId, intValue)) {
|
||||||
|
prepareForNextElement();
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TYPE_FLOAT:
|
||||||
|
if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
|
||||||
|
&& elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
|
||||||
|
throw new IllegalStateException("Invalid float size " + elementContentSize);
|
||||||
|
}
|
||||||
|
int floatResult =
|
||||||
|
readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
|
||||||
|
if (floatResult != READ_RESULT_CONTINUE) {
|
||||||
|
return floatResult;
|
||||||
|
}
|
||||||
|
long valueBits = getTempByteArrayValue((int) elementContentSize, false);
|
||||||
|
double floatValue;
|
||||||
|
if (elementContentSize == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
|
||||||
|
floatValue = Float.intBitsToFloat((int) valueBits);
|
||||||
|
} else {
|
||||||
|
floatValue = Double.longBitsToDouble(valueBits);
|
||||||
|
}
|
||||||
|
if (!eventHandler.onFloatElement(elementId, floatValue)) {
|
||||||
|
prepareForNextElement();
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
if (elementContentSize > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"String element size " + elementContentSize + " is larger than MAX_INT");
|
||||||
|
}
|
||||||
|
if (stringBytes == null) {
|
||||||
|
stringBytes = new byte[(int) elementContentSize];
|
||||||
|
}
|
||||||
|
int stringResult =
|
||||||
|
readBytesInternal(inputStream, stringBytes, (int) elementContentSize);
|
||||||
|
if (stringResult != READ_RESULT_CONTINUE) {
|
||||||
|
return stringResult;
|
||||||
|
}
|
||||||
|
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
|
||||||
|
stringBytes = null;
|
||||||
|
if (!eventHandler.onStringElement(elementId, stringValue)) {
|
||||||
|
prepareForNextElement();
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TYPE_BINARY:
|
||||||
|
if (elementContentSize > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Binary element size " + elementContentSize + " is larger than MAX_INT");
|
||||||
|
}
|
||||||
|
if (inputStream.getAvailableByteCount() < elementContentSize) {
|
||||||
|
return READ_RESULT_NEED_MORE_DATA;
|
||||||
|
}
|
||||||
|
int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
|
||||||
|
boolean keepGoing = eventHandler.onBinaryElement(
|
||||||
|
elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
|
||||||
|
long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
|
||||||
|
if (expectedBytesRead != bytesRead) {
|
||||||
|
throw new IllegalStateException("Incorrect total bytes read. Expected "
|
||||||
|
+ expectedBytesRead + " but actually " + bytesRead);
|
||||||
|
}
|
||||||
|
if (!keepGoing) {
|
||||||
|
prepareForNextElement();
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TYPE_UNKNOWN:
|
||||||
|
if (elementContentSize > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Unknown element size " + elementContentSize + " is larger than MAX_INT");
|
||||||
|
}
|
||||||
|
int skipResult = skipBytesInternal(inputStream, (int) elementContentSize);
|
||||||
|
if (skipResult != READ_RESULT_CONTINUE) {
|
||||||
|
return skipResult;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Invalid element type " + type);
|
||||||
|
}
|
||||||
|
prepareForNextElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBytesRead() {
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
prepareForNextElement();
|
||||||
|
masterElementsStack.clear();
|
||||||
|
bytesRead = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long readVarint(NonBlockingInputStream inputStream) {
|
||||||
|
varintBytesState = STATE_BEGIN_READING;
|
||||||
|
int result = readVarintBytes(inputStream);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
throw new IllegalStateException("Couldn't read varint");
|
||||||
|
}
|
||||||
|
return getTempByteArrayValue(varintBytesLength, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
|
||||||
|
bytesState = 0;
|
||||||
|
int result = readBytesInternal(inputStream, byteBuffer, totalBytes);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
throw new IllegalStateException("Couldn't read bytes into buffer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
|
||||||
|
bytesState = 0;
|
||||||
|
int result = readBytesInternal(inputStream, byteArray, totalBytes);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
throw new IllegalStateException("Couldn't read bytes into array");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
|
||||||
|
bytesState = 0;
|
||||||
|
int result = skipBytesInternal(inputStream, totalBytes);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
throw new IllegalStateException("Couldn't skip bytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
|
||||||
|
* reading a new element from scratch.
|
||||||
|
*/
|
||||||
|
private void prepareForNextElement() {
|
||||||
|
state = STATE_BEGIN_READING;
|
||||||
|
elementIdState = STATE_BEGIN_READING;
|
||||||
|
elementContentSizeState = STATE_BEGIN_READING;
|
||||||
|
elementOffset = bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an element ID such that reading can be stopped and started again in a later call
|
||||||
|
* if not enough bytes are available. Returns {@link #READ_RESULT_CONTINUE} if a full element ID
|
||||||
|
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
|
||||||
|
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which an element ID should be read
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int readElementId(NonBlockingInputStream inputStream) {
|
||||||
|
if (elementIdState == STATE_FINISHED_READING) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
if (elementIdState == STATE_BEGIN_READING) {
|
||||||
|
varintBytesState = STATE_BEGIN_READING;
|
||||||
|
elementIdState = STATE_READ_CONTENTS;
|
||||||
|
}
|
||||||
|
int result = readVarintBytes(inputStream);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Element IDs are at most 4 bytes so cast to int now.
|
||||||
|
elementId = (int) getTempByteArrayValue(varintBytesLength, false);
|
||||||
|
elementIdState = STATE_FINISHED_READING;
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an element's content size such that reading can be stopped and started again in a later
|
||||||
|
* call if not enough bytes are available.
|
||||||
|
*
|
||||||
|
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire element size has been
|
||||||
|
* read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
|
||||||
|
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which an element size should be read
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int readElementContentSize(NonBlockingInputStream inputStream) {
|
||||||
|
if (elementContentSizeState == STATE_FINISHED_READING) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
if (elementContentSizeState == STATE_BEGIN_READING) {
|
||||||
|
varintBytesState = STATE_BEGIN_READING;
|
||||||
|
elementContentSizeState = STATE_READ_CONTENTS;
|
||||||
|
}
|
||||||
|
int result = readVarintBytes(inputStream);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
elementContentSize = getTempByteArrayValue(varintBytesLength, true);
|
||||||
|
elementContentSizeState = STATE_FINISHED_READING;
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
|
||||||
|
* again in a later call if not enough bytes are available.
|
||||||
|
*
|
||||||
|
* <p>Returns {@link #READ_RESULT_CONTINUE} if an entire varint has been read into
|
||||||
|
* {@link #tempByteArray} and the length of the varint is in {@link #varintBytesLength}.
|
||||||
|
* Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} before calling to indicate
|
||||||
|
* a new varint should be read.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which a varint should be read
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int readVarintBytes(NonBlockingInputStream inputStream) {
|
||||||
|
if (varintBytesState == STATE_FINISHED_READING) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read first byte to get length.
|
||||||
|
if (varintBytesState == STATE_BEGIN_READING) {
|
||||||
|
bytesState = 0;
|
||||||
|
int result = readBytesInternal(inputStream, tempByteArray, 1);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
varintBytesState = STATE_READ_CONTENTS;
|
||||||
|
|
||||||
|
int firstByte = tempByteArray[0] & 0xff;
|
||||||
|
varintBytesLength = -1;
|
||||||
|
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
|
||||||
|
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
|
||||||
|
varintBytesLength = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (varintBytesLength == -1) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"No valid varint length mask found at bytesRead = " + bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read remaining bytes.
|
||||||
|
int result = readBytesInternal(inputStream, tempByteArray, varintBytesLength);
|
||||||
|
if (result != READ_RESULT_CONTINUE) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All bytes have been read.
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a set amount of bytes into a {@link ByteBuffer} such that reading can be stopped
|
||||||
|
* and started again later if not enough bytes are available.
|
||||||
|
*
|
||||||
|
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
|
||||||
|
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which bytes should be read
|
||||||
|
* @param byteBuffer The {@link ByteBuffer} into which bytes should be read
|
||||||
|
* @param totalBytes The total size of bytes to be read
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int readBytesInternal(
|
||||||
|
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
|
||||||
|
if (bytesState == STATE_BEGIN_READING && totalBytes > byteBuffer.capacity()) {
|
||||||
|
throw new IllegalArgumentException("Byte buffer not large enough");
|
||||||
|
}
|
||||||
|
if (bytesState >= totalBytes) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
int remainingBytes = totalBytes - bytesState;
|
||||||
|
int additionalBytesRead = inputStream.read(byteBuffer, remainingBytes);
|
||||||
|
return updateBytesState(additionalBytesRead, totalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a set amount of bytes into a {@code byte[]} such that reading can be stopped
|
||||||
|
* and started again later if not enough bytes are available.
|
||||||
|
*
|
||||||
|
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
|
||||||
|
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which bytes should be read
|
||||||
|
* @param byteArray The {@code byte[]} into which bytes should be read
|
||||||
|
* @param totalBytes The total size of bytes to be read
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int readBytesInternal(
|
||||||
|
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
|
||||||
|
if (bytesState == STATE_BEGIN_READING && totalBytes > byteArray.length) {
|
||||||
|
throw new IllegalArgumentException("Byte array not large enough");
|
||||||
|
}
|
||||||
|
if (bytesState >= totalBytes) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
int remainingBytes = totalBytes - bytesState;
|
||||||
|
int additionalBytesRead = inputStream.read(byteArray, bytesState, remainingBytes);
|
||||||
|
return updateBytesState(additionalBytesRead, totalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips a set amount of bytes such that reading can be stopped and started again later if
|
||||||
|
* not enough bytes are available.
|
||||||
|
*
|
||||||
|
* <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been skipped. Reset
|
||||||
|
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes
|
||||||
|
* should be skipped.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream from which bytes should be skipped
|
||||||
|
* @param totalBytes The total size of bytes to be skipped
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int skipBytesInternal(NonBlockingInputStream inputStream, int totalBytes) {
|
||||||
|
if (bytesState >= totalBytes) {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
int remainingBytes = totalBytes - bytesState;
|
||||||
|
int additionalBytesRead = inputStream.skip(remainingBytes);
|
||||||
|
return updateBytesState(additionalBytesRead, totalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates {@link #bytesState} and {@link #bytesRead} after reading bytes in one of the
|
||||||
|
* {@code verbBytesInternal} methods.
|
||||||
|
*
|
||||||
|
* @param additionalBytesRead The number of additional bytes read to be accounted for
|
||||||
|
* @param totalBytes The total size of bytes to be read or skipped
|
||||||
|
* @return One of the {@code RESULT_*} flags defined in this class
|
||||||
|
*/
|
||||||
|
private int updateBytesState(int additionalBytesRead, int totalBytes) {
|
||||||
|
if (additionalBytesRead == -1) {
|
||||||
|
return READ_RESULT_END_OF_FILE;
|
||||||
|
}
|
||||||
|
bytesState += additionalBytesRead;
|
||||||
|
bytesRead += additionalBytesRead;
|
||||||
|
if (bytesState < totalBytes) {
|
||||||
|
return READ_RESULT_NEED_MORE_DATA;
|
||||||
|
} else {
|
||||||
|
return READ_RESULT_CONTINUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
|
||||||
|
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
|
||||||
|
*
|
||||||
|
* @param byteLength The number of bytes to parse from {@link #tempByteArray}
|
||||||
|
* @param removeLengthMask Removes the variable-length integer length mask from the value
|
||||||
|
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used
|
||||||
|
*/
|
||||||
|
private long getTempByteArrayValue(int byteLength, boolean removeLengthMask) {
|
||||||
|
if (removeLengthMask) {
|
||||||
|
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
|
||||||
|
}
|
||||||
|
long varint = 0;
|
||||||
|
for (int i = 0; i < byteLength; i++) {
|
||||||
|
// Shift all existing bits up one byte and add the next byte at the bottom.
|
||||||
|
varint = (varint << 8) | (tempByteArray[i] & 0xff);
|
||||||
|
}
|
||||||
|
return varint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in {@link #masterElementsStack} to track when the current master element ends so that
|
||||||
|
* {@link EbmlEventHandler#onMasterElementEnd(int)} is called.
|
||||||
|
*/
|
||||||
|
private static final class MasterElement {
|
||||||
|
|
||||||
|
private final int elementId;
|
||||||
|
private final long elementEndOffsetBytes;
|
||||||
|
|
||||||
|
private MasterElement(int elementId, long elementEndOffsetBytes) {
|
||||||
|
this.elementId = elementId;
|
||||||
|
this.elementEndOffsetBytes = elementEndOffsetBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,427 @@
|
|||||||
|
/*
|
||||||
|
* 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.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 final EbmlReader reader;
|
||||||
|
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
|
||||||
|
|
||||||
|
private SampleHolder tempSampleHolder;
|
||||||
|
private boolean sampleRead;
|
||||||
|
|
||||||
|
private boolean prepared = false;
|
||||||
|
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());
|
||||||
|
this.cueTimesUs = new LongArray();
|
||||||
|
this.cueClusterPositions = new LongArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPrepared() {
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
|
||||||
|
tempSampleHolder = sampleHolder;
|
||||||
|
sampleRead = false;
|
||||||
|
reader.read(inputStream);
|
||||||
|
tempSampleHolder = null;
|
||||||
|
return sampleRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
|
||||||
|
checkPrepared();
|
||||||
|
if (allowNoop
|
||||||
|
&& simpleBlockTimecodeUs != UNKNOWN
|
||||||
|
&& seekTimeUs >= simpleBlockTimecodeUs) {
|
||||||
|
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
|
||||||
|
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SegmentIndex getCues() {
|
||||||
|
checkPrepared();
|
||||||
|
return cues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaFormat getFormat() {
|
||||||
|
checkPrepared();
|
||||||
|
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;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ boolean onMasterElementEnd(int id) {
|
||||||
|
if (id == ID_CUES) {
|
||||||
|
finishPreparing();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
||||||
|
tempSampleHolder.decodeOnly = invisible;
|
||||||
|
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
|
||||||
|
tempSampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
|
||||||
|
break;
|
||||||
|
case LACING_EBML:
|
||||||
|
case LACING_FIXED:
|
||||||
|
case LACING_XIPH:
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read video data into sample holder.
|
||||||
|
reader.readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
|
||||||
|
sampleRead = true;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
reader.skipBytes(inputStream, contentsSizeBytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long scaleTimecodeToUs(long unscaledTimecode) {
|
||||||
|
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkPrepared() {
|
||||||
|
if (!prepared) {
|
||||||
|
throw new IllegalStateException("Parser not yet prepared");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishPreparing() {
|
||||||
|
if (prepared) {
|
||||||
|
throw new IllegalStateException("Already prepared");
|
||||||
|
} else if (segmentStartOffsetBytes == UNKNOWN) {
|
||||||
|
throw new IllegalStateException("Segment start/end offsets unknown");
|
||||||
|
} else if (durationUs == UNKNOWN) {
|
||||||
|
throw new IllegalStateException("Duration unknown");
|
||||||
|
} else if (pixelWidth == UNKNOWN || pixelHeight == UNKNOWN) {
|
||||||
|
throw new IllegalStateException("Pixel width/height unknown");
|
||||||
|
} else if (cuesSizeBytes == UNKNOWN) {
|
||||||
|
throw new IllegalStateException("Cues size unknown");
|
||||||
|
} else if (cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
|
||||||
|
throw new IllegalStateException("Invalid/missing cue points");
|
||||||
|
}
|
||||||
|
|
||||||
|
format = MediaFormat.createVideoFormat(
|
||||||
|
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 boolean onMasterElementStart(
|
||||||
|
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
|
||||||
|
return DefaultWebmExtractor.this.onMasterElementStart(
|
||||||
|
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMasterElementEnd(int id) {
|
||||||
|
return DefaultWebmExtractor.this.onMasterElementEnd(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onIntegerElement(int id, long value) {
|
||||||
|
return DefaultWebmExtractor.this.onIntegerElement(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFloatElement(int id, double value) {
|
||||||
|
return DefaultWebmExtractor.this.onFloatElement(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStringElement(int id, String value) {
|
||||||
|
return 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* 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.upstream.NonBlockingInputStream;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines EBML element IDs/types and reacts to events.
|
||||||
|
*/
|
||||||
|
/* package */ interface EbmlEventHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the type of an element ID.
|
||||||
|
*
|
||||||
|
* <p>If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped.
|
||||||
|
* Note that all children of a skipped master element are also skipped.
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @return One of the {@code TYPE_} constants defined in this class
|
||||||
|
*/
|
||||||
|
public int getElementType(int id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
|
||||||
|
*
|
||||||
|
* <p>Following events should be considered as taking place "within" this element until a
|
||||||
|
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it is possible for
|
||||||
|
* another master element of the same ID to be nested within itself.
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @param elementOffsetBytes The byte offset where this element starts
|
||||||
|
* @param headerSizeBytes The byte length of this element's ID and size header
|
||||||
|
* @param contentsSizeBytes The byte length of this element's children
|
||||||
|
* @return {@code false} if parsing should stop right away
|
||||||
|
*/
|
||||||
|
public boolean onMasterElementStart(
|
||||||
|
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a master element has finished reading in all of its children from the
|
||||||
|
* {@link NonBlockingInputStream}.
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @return {@code false} if parsing should stop right away
|
||||||
|
*/
|
||||||
|
public boolean onMasterElementEnd(int id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @param value The integer value this element contains
|
||||||
|
* @return {@code false} if parsing should stop right away
|
||||||
|
*/
|
||||||
|
public boolean onIntegerElement(int id, long value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @param value The float value this element contains
|
||||||
|
* @return {@code false} if parsing should stop right away
|
||||||
|
*/
|
||||||
|
public boolean onFloatElement(int id, double value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @param value The string value this element contains
|
||||||
|
* @return {@code false} if parsing should stop right away
|
||||||
|
*/
|
||||||
|
public boolean onStringElement(int id, String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
|
||||||
|
*
|
||||||
|
* <p>The element header (containing element ID and content size) will already have been read.
|
||||||
|
* Subclasses must exactly read the entire contents of the element, which is
|
||||||
|
* {@code contentsSizeBytes} in length. It's guaranteed that the full element contents will be
|
||||||
|
* immediately available from {@code inputStream}.
|
||||||
|
*
|
||||||
|
* <p>Several methods in {@link EbmlReader} are available for reading the contents of a
|
||||||
|
* binary element:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link EbmlReader#readVarint(NonBlockingInputStream)}.
|
||||||
|
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, byte[], int)}.
|
||||||
|
* <li>{@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}.
|
||||||
|
* <li>{@link EbmlReader#skipBytes(NonBlockingInputStream, int)}.
|
||||||
|
* <li>{@link EbmlReader#getBytesRead()}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param id The integer ID of this element
|
||||||
|
* @param elementOffsetBytes The byte offset where this element starts
|
||||||
|
* @param headerSizeBytes The byte length of this element's ID and size header
|
||||||
|
* @param contentsSizeBytes The byte length of this element's contents
|
||||||
|
* @param inputStream The {@link NonBlockingInputStream} from which this
|
||||||
|
* element's contents should be read
|
||||||
|
* @return {@code false} if parsing should stop right away
|
||||||
|
*/
|
||||||
|
public boolean onBinaryElement(
|
||||||
|
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
|
||||||
|
NonBlockingInputStream inputStream);
|
||||||
|
|
||||||
|
}
|
@ -16,528 +16,92 @@
|
|||||||
package com.google.android.exoplayer.parser.webm;
|
package com.google.android.exoplayer.parser.webm;
|
||||||
|
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.Stack;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event-driven incremental EBML reader base class.
|
* Basic event-driven incremental EBML parser which needs an {@link EbmlEventHandler} to
|
||||||
|
* define IDs/types and react to events.
|
||||||
*
|
*
|
||||||
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
|
* <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
|
||||||
* It was originally designed for the Matroska container format. More information about EBML and
|
* It was originally designed for the Matroska container format. More information about EBML and
|
||||||
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
|
||||||
*/
|
*/
|
||||||
public abstract class EbmlReader {
|
/* package */ interface EbmlReader {
|
||||||
|
|
||||||
// Element Types
|
// Element Types
|
||||||
protected static final int TYPE_UNKNOWN = 0; // Undefined element.
|
/** Undefined element. */
|
||||||
protected static final int TYPE_MASTER = 1; // Contains child elements.
|
public static final int TYPE_UNKNOWN = 0;
|
||||||
protected static final int TYPE_UNSIGNED_INT = 2;
|
/** Contains child elements. */
|
||||||
protected static final int TYPE_STRING = 3;
|
public static final int TYPE_MASTER = 1;
|
||||||
protected static final int TYPE_BINARY = 4;
|
/** Unsigned integer value of up to 8 bytes. */
|
||||||
protected static final int TYPE_FLOAT = 5;
|
public static final int TYPE_UNSIGNED_INT = 2;
|
||||||
|
public static final int TYPE_STRING = 3;
|
||||||
|
public static final int TYPE_BINARY = 4;
|
||||||
|
/** IEEE floating point value of either 4 or 8 bytes. */
|
||||||
|
public static final int TYPE_FLOAT = 5;
|
||||||
|
|
||||||
// Return values for methods read, readElementId, readElementSize, readVarintBytes, and readBytes.
|
// Return values for reading methods.
|
||||||
protected static final int RESULT_CONTINUE = 0;
|
public static final int READ_RESULT_CONTINUE = 0;
|
||||||
protected static final int RESULT_NEED_MORE_DATA = 1;
|
public static final int READ_RESULT_NEED_MORE_DATA = 1;
|
||||||
protected static final int RESULT_END_OF_FILE = 2;
|
public static final int READ_RESULT_END_OF_FILE = 2;
|
||||||
|
|
||||||
// State values used in variables state, elementIdState, elementContentSizeState, and
|
public void setEventHandler(EbmlEventHandler eventHandler);
|
||||||
// varintBytesState.
|
|
||||||
private static final int STATE_BEGIN_READING = 0;
|
|
||||||
private static final int STATE_READ_CONTENTS = 1;
|
|
||||||
private static final int STATE_FINISHED_READING = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The first byte of a variable-length integer (varint) will have one of these bit masks
|
|
||||||
* indicating the total length in bytes. {@code 0x80} is a one-byte integer,
|
|
||||||
* {@code 0x40} is two bytes, and so on up to eight bytes.
|
|
||||||
*/
|
|
||||||
private static final int[] VARINT_LENGTH_MASKS = new int[] {
|
|
||||||
0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
|
|
||||||
};
|
|
||||||
|
|
||||||
private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
|
|
||||||
private final byte[] tempByteArray = new byte[8];
|
|
||||||
|
|
||||||
private int state;
|
|
||||||
private long bytesRead;
|
|
||||||
private long elementOffset;
|
|
||||||
private int elementId;
|
|
||||||
private int elementIdState;
|
|
||||||
private long elementContentSize;
|
|
||||||
private int elementContentSizeState;
|
|
||||||
private int varintBytesState;
|
|
||||||
private int varintBytesLength;
|
|
||||||
private int bytesState;
|
|
||||||
private byte[] stringBytes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to retrieve the type of an element ID. If {@link #TYPE_UNKNOWN} is returned then
|
|
||||||
* the element is skipped. Note that all children of a skipped master element are also skipped.
|
|
||||||
*
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @return One of the {@code TYPE_} constants defined in this class.
|
|
||||||
*/
|
|
||||||
protected abstract int getElementType(int id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a master element is encountered in the {@link NonBlockingInputStream}.
|
|
||||||
* Following events should be considered as taking place "within" this element until a
|
|
||||||
* matching call to {@link #onMasterElementEnd(int)} is made. Note that it
|
|
||||||
* is possible for the same master element to be nested within itself.
|
|
||||||
*
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @param elementOffset The byte offset where this element starts.
|
|
||||||
* @param headerSize The byte length of this element's ID and size header.
|
|
||||||
* @param contentsSize The byte length of this element's children.
|
|
||||||
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onMasterElementStart(
|
|
||||||
int id, long elementOffset, int headerSize, int contentsSize);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a master element has finished reading in all of its children from the
|
|
||||||
* {@link NonBlockingInputStream}.
|
|
||||||
*
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onMasterElementEnd(int id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an integer element is encountered in the {@link NonBlockingInputStream}.
|
|
||||||
*
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @param value The integer value this element contains.
|
|
||||||
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onIntegerElement(int id, long value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a float element is encountered in the {@link NonBlockingInputStream}.
|
|
||||||
*
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @param value The float value this element contains.
|
|
||||||
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onFloatElement(int id, double value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a string element is encountered in the {@link NonBlockingInputStream}.
|
|
||||||
*
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @param value The string value this element contains.
|
|
||||||
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onStringElement(int id, String value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a binary element is encountered in the {@link NonBlockingInputStream}.
|
|
||||||
* The element header (containing element ID and content size) will already have been read.
|
|
||||||
* Subclasses must exactly read the entire contents of the element, which is {@code contentsSize}
|
|
||||||
* bytes in length. It's guaranteed that the full element contents will be immediately available
|
|
||||||
* from {@code inputStream}.
|
|
||||||
*
|
|
||||||
* <p>Several methods are available for reading the contents of a binary element:
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link #readVarint(NonBlockingInputStream)}.
|
|
||||||
* <li>{@link #readBytes(NonBlockingInputStream, byte[], int)}.
|
|
||||||
* <li>{@link #readBytes(NonBlockingInputStream, ByteBuffer, int)}.
|
|
||||||
* <li>{@link #skipBytes(NonBlockingInputStream, int)}.
|
|
||||||
* <li>{@link #getBytesRead()}.
|
|
||||||
*
|
|
||||||
* @param inputStream The {@link NonBlockingInputStream} from which this
|
|
||||||
* element's contents should be read.
|
|
||||||
* @param id The integer ID of this element.
|
|
||||||
* @param elementOffset The byte offset where this element starts.
|
|
||||||
* @param headerSize The byte length of this element's ID and size header.
|
|
||||||
* @param contentsSize The byte length of this element's contents.
|
|
||||||
* @return {@code true} if parsing should continue or {@code false} if it should stop right away.
|
|
||||||
*/
|
|
||||||
protected abstract boolean onBinaryElement(NonBlockingInputStream inputStream,
|
|
||||||
int id, long elementOffset, int headerSize, int contentsSize);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
|
* Reads from a {@link NonBlockingInputStream} and calls event callbacks as needed.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @param inputStream The input stream from which data should be read
|
||||||
* @return One of the {@code RESULT_*} flags defined in this class.
|
* @return One of the {@code RESULT_*} flags defined in this interface
|
||||||
*/
|
*/
|
||||||
protected final int read(NonBlockingInputStream inputStream) {
|
public int read(NonBlockingInputStream inputStream);
|
||||||
while (true) {
|
|
||||||
while (masterElementsStack.size() > 0
|
|
||||||
&& bytesRead >= masterElementsStack.peek().elementEndOffset) {
|
|
||||||
if (!onMasterElementEnd(masterElementsStack.pop().elementId)) {
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == STATE_BEGIN_READING) {
|
|
||||||
final int resultId = readElementId(inputStream);
|
|
||||||
if (resultId != RESULT_CONTINUE) {
|
|
||||||
return resultId;
|
|
||||||
}
|
|
||||||
final int resultSize = readElementContentSize(inputStream);
|
|
||||||
if (resultSize != RESULT_CONTINUE) {
|
|
||||||
return resultSize;
|
|
||||||
}
|
|
||||||
state = STATE_READ_CONTENTS;
|
|
||||||
bytesState = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int type = getElementType(elementId);
|
|
||||||
switch (type) {
|
|
||||||
|
|
||||||
case TYPE_MASTER:
|
|
||||||
final int masterHeaderSize = (int) (bytesRead - elementOffset);
|
|
||||||
masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
|
|
||||||
if (!onMasterElementStart(
|
|
||||||
elementId, elementOffset, masterHeaderSize, (int) elementContentSize)) {
|
|
||||||
prepareForNextElement();
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TYPE_UNSIGNED_INT:
|
|
||||||
Assertions.checkState(elementContentSize <= 8);
|
|
||||||
final int resultInt =
|
|
||||||
readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
|
|
||||||
if (resultInt != RESULT_CONTINUE) {
|
|
||||||
return resultInt;
|
|
||||||
}
|
|
||||||
final long intValue = parseTempByteArray((int) elementContentSize, false);
|
|
||||||
if (!onIntegerElement(elementId, intValue)) {
|
|
||||||
prepareForNextElement();
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TYPE_FLOAT:
|
|
||||||
Assertions.checkState(elementContentSize == 4 || elementContentSize == 8);
|
|
||||||
final int resultFloat =
|
|
||||||
readBytes(inputStream, null, tempByteArray, (int) elementContentSize);
|
|
||||||
if (resultFloat != RESULT_CONTINUE) {
|
|
||||||
return resultFloat;
|
|
||||||
}
|
|
||||||
final long valueBits = parseTempByteArray((int) elementContentSize, false);
|
|
||||||
final double floatValue;
|
|
||||||
if (elementContentSize == 4) {
|
|
||||||
floatValue = Float.intBitsToFloat((int) valueBits);
|
|
||||||
} else {
|
|
||||||
floatValue = Double.longBitsToDouble(valueBits);
|
|
||||||
}
|
|
||||||
if (!onFloatElement(elementId, floatValue)) {
|
|
||||||
prepareForNextElement();
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TYPE_STRING:
|
|
||||||
if (stringBytes == null) {
|
|
||||||
stringBytes = new byte[(int) elementContentSize];
|
|
||||||
}
|
|
||||||
final int resultString =
|
|
||||||
readBytes(inputStream, null, stringBytes, (int) elementContentSize);
|
|
||||||
if (resultString != RESULT_CONTINUE) {
|
|
||||||
return resultString;
|
|
||||||
}
|
|
||||||
final String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
|
|
||||||
stringBytes = null;
|
|
||||||
if (!onStringElement(elementId, stringValue)) {
|
|
||||||
prepareForNextElement();
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TYPE_BINARY:
|
|
||||||
if (inputStream.getAvailableByteCount() < elementContentSize) {
|
|
||||||
return RESULT_NEED_MORE_DATA;
|
|
||||||
}
|
|
||||||
final int binaryHeaderSize = (int) (bytesRead - elementOffset);
|
|
||||||
final boolean keepGoing = onBinaryElement(
|
|
||||||
inputStream, elementId, elementOffset, binaryHeaderSize, (int) elementContentSize);
|
|
||||||
Assertions.checkState(elementOffset + binaryHeaderSize + elementContentSize == bytesRead);
|
|
||||||
if (!keepGoing) {
|
|
||||||
prepareForNextElement();
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TYPE_UNKNOWN:
|
|
||||||
// Unknown elements should be skipped.
|
|
||||||
Assertions.checkState(
|
|
||||||
readBytes(inputStream, null, null, (int) elementContentSize) == RESULT_CONTINUE);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Invalid element type " + type);
|
|
||||||
|
|
||||||
}
|
|
||||||
prepareForNextElement();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The total number of bytes consumed by the reader since first created
|
* The total number of bytes consumed by the reader since first created or last {@link #reset()}.
|
||||||
* or last {@link #reset()}.
|
|
||||||
*/
|
*/
|
||||||
protected final long getBytesRead() {
|
public long getBytesRead();
|
||||||
return bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the entire state of the reader so that it will read a new EBML structure from scratch.
|
* Resets the entire state of the reader so that it will read a new EBML structure from scratch.
|
||||||
* This includes resetting {@link #bytesRead} back to 0 and discarding all pending
|
*
|
||||||
* {@link #onMasterElementEnd(int)} events.
|
* <p>This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
|
||||||
|
* all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events.
|
||||||
*/
|
*/
|
||||||
protected final void reset() {
|
public void reset();
|
||||||
prepareForNextElement();
|
|
||||||
masterElementsStack.clear();
|
|
||||||
bytesRead = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents
|
* Reads, parses, and returns an EBML variable-length integer (varint) from the contents
|
||||||
* of a binary element.
|
* of a binary element.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @param inputStream The input stream from which data should be read
|
||||||
* @return The varint value at the current position of the contents of a binary element.
|
* @return The varint value at the current position of the contents of a binary element
|
||||||
*/
|
*/
|
||||||
protected final long readVarint(NonBlockingInputStream inputStream) {
|
public long readVarint(NonBlockingInputStream inputStream);
|
||||||
varintBytesState = STATE_BEGIN_READING;
|
|
||||||
Assertions.checkState(readVarintBytes(inputStream) == RESULT_CONTINUE);
|
|
||||||
return parseTempByteArray(varintBytesLength, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
|
* Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @param inputStream The input stream from which data should be read
|
||||||
* @param byteBuffer The {@link ByteBuffer} to which data should be written.
|
* @param byteBuffer The {@link ByteBuffer} to which data should be written
|
||||||
* @param totalBytes The fixed number of bytes to be read and written.
|
* @param totalBytes The fixed number of bytes to be read and written
|
||||||
*/
|
*/
|
||||||
protected final void readBytes(
|
public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
|
||||||
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
|
|
||||||
bytesState = 0;
|
|
||||||
Assertions.checkState(readBytes(inputStream, byteBuffer, null, totalBytes) == RESULT_CONTINUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
|
* Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @param inputStream The input stream from which data should be read
|
||||||
* @param byteArray The byte array to which data should be written.
|
* @param byteArray The byte array to which data should be written
|
||||||
* @param totalBytes The fixed number of bytes to be read and written.
|
* @param totalBytes The fixed number of bytes to be read and written
|
||||||
*/
|
*/
|
||||||
protected final void readBytes(
|
public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
|
||||||
NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
|
|
||||||
bytesState = 0;
|
|
||||||
Assertions.checkState(readBytes(inputStream, null, byteArray, totalBytes) == RESULT_CONTINUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skips a fixed number of bytes from the contents of a binary element.
|
* Skips a fixed number of bytes from the contents of a binary element.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be skipped.
|
* @param inputStream The input stream from which data should be skipped
|
||||||
* @param totalBytes The fixed number of bytes to be skipped.
|
* @param totalBytes The fixed number of bytes to be skipped
|
||||||
*/
|
*/
|
||||||
protected final void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
|
public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
|
||||||
bytesState = 0;
|
|
||||||
Assertions.checkState(readBytes(inputStream, null, null, totalBytes) == RESULT_CONTINUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
|
|
||||||
* reading a new element from scratch.
|
|
||||||
*/
|
|
||||||
private final void prepareForNextElement() {
|
|
||||||
state = STATE_BEGIN_READING;
|
|
||||||
elementIdState = STATE_BEGIN_READING;
|
|
||||||
elementContentSizeState = STATE_BEGIN_READING;
|
|
||||||
elementOffset = bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads an element ID such that reading can be stopped and started again in a later call
|
|
||||||
* if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if a full element ID
|
|
||||||
* has been read into {@link #elementId}. Reset {@link #elementIdState} to
|
|
||||||
* {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
|
|
||||||
*
|
|
||||||
* @param inputStream The input stream from which an element ID should be read.
|
|
||||||
* @return One of the {@code RESULT_*} flags defined in this class.
|
|
||||||
*/
|
|
||||||
private int readElementId(NonBlockingInputStream inputStream) {
|
|
||||||
if (elementIdState == STATE_FINISHED_READING) {
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
if (elementIdState == STATE_BEGIN_READING) {
|
|
||||||
varintBytesState = STATE_BEGIN_READING;
|
|
||||||
elementIdState = STATE_READ_CONTENTS;
|
|
||||||
}
|
|
||||||
final int result = readVarintBytes(inputStream);
|
|
||||||
if (result != RESULT_CONTINUE) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
elementId = (int) parseTempByteArray(varintBytesLength, false);
|
|
||||||
elementIdState = STATE_FINISHED_READING;
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads an element's content size such that reading can be stopped and started again in a later
|
|
||||||
* call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if an entire element
|
|
||||||
* size has been read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
|
|
||||||
* {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
|
|
||||||
*
|
|
||||||
* @param inputStream The input stream from which an element size should be read.
|
|
||||||
* @return One of the {@code RESULT_*} flags defined in this class.
|
|
||||||
*/
|
|
||||||
private int readElementContentSize(NonBlockingInputStream inputStream) {
|
|
||||||
if (elementContentSizeState == STATE_FINISHED_READING) {
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
if (elementContentSizeState == STATE_BEGIN_READING) {
|
|
||||||
varintBytesState = STATE_BEGIN_READING;
|
|
||||||
elementContentSizeState = STATE_READ_CONTENTS;
|
|
||||||
}
|
|
||||||
final int result = readVarintBytes(inputStream);
|
|
||||||
if (result != RESULT_CONTINUE) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
elementContentSize = parseTempByteArray(varintBytesLength, true);
|
|
||||||
elementContentSizeState = STATE_FINISHED_READING;
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads an EBML variable-length integer (varint) such that reading can be stopped and started
|
|
||||||
* again in a later call if not enough bytes are available. Returns {@link #RESULT_CONTINUE} if
|
|
||||||
* an entire varint has been read into {@link #tempByteArray} and the length of the varint is in
|
|
||||||
* {@link #varintBytesLength}. Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING}
|
|
||||||
* before calling to indicate a new varint should be read.
|
|
||||||
*
|
|
||||||
* @param inputStream The input stream from which a varint should be read.
|
|
||||||
* @return One of the {@code RESULT_*} flags defined in this class.
|
|
||||||
*/
|
|
||||||
private int readVarintBytes(NonBlockingInputStream inputStream) {
|
|
||||||
if (varintBytesState == STATE_FINISHED_READING) {
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read first byte to get length.
|
|
||||||
if (varintBytesState == STATE_BEGIN_READING) {
|
|
||||||
bytesState = 0;
|
|
||||||
final int result = readBytes(inputStream, null, tempByteArray, 1);
|
|
||||||
if (result != RESULT_CONTINUE) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
varintBytesState = STATE_READ_CONTENTS;
|
|
||||||
|
|
||||||
final int firstByte = tempByteArray[0] & 0xff;
|
|
||||||
varintBytesLength = -1;
|
|
||||||
for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
|
|
||||||
if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
|
|
||||||
varintBytesLength = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (varintBytesLength == -1) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"No valid varint length mask found at bytesRead = " + bytesRead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read remaining bytes.
|
|
||||||
final int result = readBytes(inputStream, null, tempByteArray, varintBytesLength);
|
|
||||||
if (result != RESULT_CONTINUE) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All bytes have been read.
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a set amount of bytes into a {@link ByteBuffer}, {@code byte[]}, or nowhere (skipping
|
|
||||||
* the bytes) such that reading can be stopped and started again later if not enough bytes are
|
|
||||||
* available. Returns {@link #RESULT_CONTINUE} if all bytes have been read. Reset
|
|
||||||
* {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
|
|
||||||
*
|
|
||||||
* <p>If both {@code byteBuffer} and {@code byteArray} are not null then bytes are only read
|
|
||||||
* into {@code byteBuffer}.
|
|
||||||
*
|
|
||||||
* @param inputStream The input stream from which bytes should be read.
|
|
||||||
* @param byteBuffer The optional {@link ByteBuffer} into which bytes should be read.
|
|
||||||
* @param byteArray The optional {@code byte[]} into which bytes should be read.
|
|
||||||
* @param totalBytes The total size of bytes to be read or skipped.
|
|
||||||
* @return One of the {@code RESULT_*} flags defined in this class.
|
|
||||||
*/
|
|
||||||
private int readBytes(
|
|
||||||
NonBlockingInputStream inputStream, ByteBuffer byteBuffer, byte[] byteArray, int totalBytes) {
|
|
||||||
if (bytesState == STATE_BEGIN_READING
|
|
||||||
&& ((byteBuffer != null && totalBytes > byteBuffer.capacity())
|
|
||||||
|| (byteArray != null && totalBytes > byteArray.length))) {
|
|
||||||
throw new IllegalStateException("Byte destination not large enough");
|
|
||||||
}
|
|
||||||
if (bytesState < totalBytes) {
|
|
||||||
final int remainingBytes = totalBytes - bytesState;
|
|
||||||
final int result;
|
|
||||||
if (byteBuffer != null) {
|
|
||||||
result = inputStream.read(byteBuffer, remainingBytes);
|
|
||||||
} else if (byteArray != null) {
|
|
||||||
result = inputStream.read(byteArray, bytesState, remainingBytes);
|
|
||||||
} else {
|
|
||||||
result = inputStream.skip(remainingBytes);
|
|
||||||
}
|
|
||||||
if (result == -1) {
|
|
||||||
return RESULT_END_OF_FILE;
|
|
||||||
}
|
|
||||||
bytesState += result;
|
|
||||||
bytesRead += result;
|
|
||||||
if (bytesState < totalBytes) {
|
|
||||||
return RESULT_NEED_MORE_DATA;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RESULT_CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and returns the integer value currently read into the first {@code byteLength} bytes
|
|
||||||
* of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
|
|
||||||
*
|
|
||||||
* @param byteLength The number of bytes to parse from {@link #tempByteArray}.
|
|
||||||
* @param removeLengthMask Removes the variable-length integer length mask from the value.
|
|
||||||
* @return The resulting integer value. This value could be up to 8-bytes so a Java long is used.
|
|
||||||
*/
|
|
||||||
private long parseTempByteArray(int byteLength, boolean removeLengthMask) {
|
|
||||||
if (removeLengthMask) {
|
|
||||||
tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
|
|
||||||
}
|
|
||||||
long varint = 0;
|
|
||||||
for (int i = 0; i < byteLength; i++) {
|
|
||||||
// Shift all existing bits up one byte and add the next byte at the bottom.
|
|
||||||
varint = (varint << 8) | (tempByteArray[i] & 0xff);
|
|
||||||
}
|
|
||||||
return varint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in {@link #masterElementsStack} to track when the current master element ends so that
|
|
||||||
* {@link #onMasterElementEnd(int)} is called.
|
|
||||||
*/
|
|
||||||
private static final class MasterElement {
|
|
||||||
|
|
||||||
private final int elementId;
|
|
||||||
private final long elementEndOffset;
|
|
||||||
|
|
||||||
private MasterElement(int elementId, long elementEndOffset) {
|
|
||||||
this.elementId = elementId;
|
|
||||||
this.elementEndOffset = elementEndOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,97 +19,22 @@ import com.google.android.exoplayer.MediaFormat;
|
|||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
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.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Facilitates the extraction of data from the WebM container format with a
|
* Extractor to facilitate data retrieval from the WebM container format.
|
||||||
* non-blocking, incremental parser based on {@link EbmlReader}.
|
|
||||||
*
|
*
|
||||||
* <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>.
|
||||||
*/
|
*/
|
||||||
@TargetApi(16)
|
public interface WebmExtractor {
|
||||||
public final class WebmExtractor extends EbmlReader {
|
|
||||||
|
|
||||||
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 final byte[] simpleBlockTimecodeAndFlags = new byte[3];
|
|
||||||
|
|
||||||
private SampleHolder tempSampleHolder;
|
|
||||||
private boolean sampleRead;
|
|
||||||
|
|
||||||
private boolean prepared = false;
|
|
||||||
private long segmentStartPosition = UNKNOWN;
|
|
||||||
private long segmentEndPosition = UNKNOWN;
|
|
||||||
private long timecodeScale = 1000000L;
|
|
||||||
private long durationUs = UNKNOWN;
|
|
||||||
private int pixelWidth = UNKNOWN;
|
|
||||||
private int pixelHeight = UNKNOWN;
|
|
||||||
private int cuesByteSize = UNKNOWN;
|
|
||||||
private long clusterTimecodeUs = UNKNOWN;
|
|
||||||
private long simpleBlockTimecodeUs = UNKNOWN;
|
|
||||||
private MediaFormat format;
|
|
||||||
private SegmentIndex cues;
|
|
||||||
private LongArray cueTimesUs;
|
|
||||||
private LongArray cueClusterPositions;
|
|
||||||
|
|
||||||
public WebmExtractor() {
|
|
||||||
cueTimesUs = new LongArray();
|
|
||||||
cueClusterPositions = new LongArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the has parsed the cues and sample format from the stream.
|
* Whether the has parsed the cues and sample format from the stream.
|
||||||
*
|
*
|
||||||
* @return True if the extractor is prepared. False otherwise.
|
* @return True if the extractor is prepared. False otherwise
|
||||||
*/
|
*/
|
||||||
public boolean isPrepared() {
|
public boolean isPrepared();
|
||||||
return prepared;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes data from a {@link NonBlockingInputStream}.
|
* Consumes data from a {@link NonBlockingInputStream}.
|
||||||
@ -118,289 +43,36 @@ public final class WebmExtractor extends EbmlReader {
|
|||||||
* {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
|
* {@code sampleHolder}. Hence the same {@link SampleHolder} instance must be passed
|
||||||
* in subsequent calls until the whole sample has been read.
|
* in subsequent calls until the whole sample has been read.
|
||||||
*
|
*
|
||||||
* @param inputStream The input stream from which data should be read.
|
* @param inputStream The input stream from which data should be read
|
||||||
* @param sampleHolder A {@link SampleHolder} into which the sample should be read.
|
* @param sampleHolder A {@link SampleHolder} into which the sample should be read
|
||||||
* @return {@code true} if a sample has been read into the sample holder, otherwise {@code false}.
|
* @return {@code true} if a sample has been read into the sample holder
|
||||||
*/
|
*/
|
||||||
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
|
public boolean read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
|
||||||
tempSampleHolder = sampleHolder;
|
|
||||||
sampleRead = false;
|
|
||||||
super.read(inputStream);
|
|
||||||
tempSampleHolder = null;
|
|
||||||
return sampleRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to a position before or equal to the requested time.
|
* Seeks to a position before or equal to the requested time.
|
||||||
*
|
*
|
||||||
* @param seekTimeUs The desired seek time in microseconds.
|
* @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
|
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
|
||||||
* segment, is equal to or greater than the time of the current sample, and if there does not
|
* 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.
|
* 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.
|
* @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);
|
||||||
checkPrepared();
|
|
||||||
if (allowNoop && simpleBlockTimecodeUs != UNKNOWN && seekTimeUs >= simpleBlockTimecodeUs) {
|
|
||||||
final int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
|
|
||||||
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reset();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cues for the media stream.
|
* Returns the cues for the media stream.
|
||||||
*
|
*
|
||||||
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
|
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
|
||||||
* prepared.
|
* prepared
|
||||||
*/
|
*/
|
||||||
public SegmentIndex getCues() {
|
public SegmentIndex getCues();
|
||||||
checkPrepared();
|
|
||||||
return cues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the format of the samples contained within the media stream.
|
* Returns the format of the samples contained within the media stream.
|
||||||
*
|
*
|
||||||
* @return The sample media format, or null if the extracted is not yet prepared.
|
* @return The sample media format, or null if the extracted is not yet prepared
|
||||||
*/
|
*/
|
||||||
public MediaFormat getFormat() {
|
public MediaFormat getFormat();
|
||||||
checkPrepared();
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onMasterElementStart(
|
|
||||||
int id, long elementOffset, int headerSize, int contentsSize) {
|
|
||||||
switch (id) {
|
|
||||||
case ID_SEGMENT:
|
|
||||||
if (segmentStartPosition != UNKNOWN || segmentEndPosition != UNKNOWN) {
|
|
||||||
throw new IllegalStateException("Multiple Segment elements not supported");
|
|
||||||
}
|
|
||||||
segmentStartPosition = elementOffset + headerSize;
|
|
||||||
segmentEndPosition = elementOffset + headerSize + contentsSize;
|
|
||||||
break;
|
|
||||||
case ID_CUES:
|
|
||||||
cuesByteSize = headerSize + contentsSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onMasterElementEnd(int id) {
|
|
||||||
switch (id) {
|
|
||||||
case ID_CUES:
|
|
||||||
finishPreparing();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected 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 IllegalStateException("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 IllegalStateException("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;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onFloatElement(int id, double value) {
|
|
||||||
switch (id) {
|
|
||||||
case ID_DURATION:
|
|
||||||
durationUs = scaleTimecodeToUs(value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected 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 IllegalStateException("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 IllegalStateException("CodecID " + value + " not supported");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onBinaryElement(NonBlockingInputStream inputStream,
|
|
||||||
int id, long elementOffset, int headerSize, int contentsSize) {
|
|
||||||
switch (id) {
|
|
||||||
case 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.
|
|
||||||
|
|
||||||
// Value of trackNumber is not used but needs to be read.
|
|
||||||
readVarint(inputStream);
|
|
||||||
|
|
||||||
// Next three bytes have timecode and flags.
|
|
||||||
readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
|
|
||||||
|
|
||||||
// First two bytes of the three are the relative timecode.
|
|
||||||
final int timecode =
|
|
||||||
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
|
|
||||||
final long timecodeUs = scaleTimecodeToUs(timecode);
|
|
||||||
|
|
||||||
// Last byte of the three has some flags and the lacing value.
|
|
||||||
final boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
|
|
||||||
final boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
|
|
||||||
final int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
|
|
||||||
//final boolean discardable = (simpleBlockTimecodeAndFlags[2] & 0x01) == 0x01; // Not used.
|
|
||||||
|
|
||||||
// Validate lacing and set info into sample holder.
|
|
||||||
switch (lacing) {
|
|
||||||
case LACING_NONE:
|
|
||||||
final long elementEndOffset = elementOffset + headerSize + contentsSize;
|
|
||||||
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
|
|
||||||
tempSampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
|
|
||||||
tempSampleHolder.decodeOnly = invisible;
|
|
||||||
tempSampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
|
|
||||||
tempSampleHolder.size = (int) (elementEndOffset - getBytesRead());
|
|
||||||
break;
|
|
||||||
case LACING_EBML:
|
|
||||||
case LACING_FIXED:
|
|
||||||
case LACING_XIPH:
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read video data into sample holder.
|
|
||||||
readBytes(inputStream, tempSampleHolder.data, tempSampleHolder.size);
|
|
||||||
sampleRead = true;
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
skipBytes(inputStream, contentsSize);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long scaleTimecodeToUs(long unscaledTimecode) {
|
|
||||||
return (unscaledTimecode * timecodeScale) / 1000L;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long scaleTimecodeToUs(double unscaledTimecode) {
|
|
||||||
return (long) ((unscaledTimecode * timecodeScale) / 1000.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkPrepared() {
|
|
||||||
if (!prepared) {
|
|
||||||
throw new IllegalStateException("Parser not yet prepared");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finishPreparing() {
|
|
||||||
if (prepared
|
|
||||||
|| segmentStartPosition == UNKNOWN || segmentEndPosition == UNKNOWN
|
|
||||||
|| durationUs == UNKNOWN
|
|
||||||
|| pixelWidth == UNKNOWN || pixelHeight == UNKNOWN
|
|
||||||
|| cuesByteSize == UNKNOWN
|
|
||||||
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
|
|
||||||
throw new IllegalStateException("Incorrect state in finishPreparing()");
|
|
||||||
}
|
|
||||||
|
|
||||||
format = MediaFormat.createVideoFormat(MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth,
|
|
||||||
pixelHeight, null);
|
|
||||||
|
|
||||||
final int cuePointsSize = cueTimesUs.size();
|
|
||||||
final int sizeBytes = cuesByteSize;
|
|
||||||
final int[] sizes = new int[cuePointsSize];
|
|
||||||
final long[] offsets = new long[cuePointsSize];
|
|
||||||
final long[] durationsUs = new long[cuePointsSize];
|
|
||||||
final long[] timesUs = new long[cuePointsSize];
|
|
||||||
for (int i = 0; i < cuePointsSize; i++) {
|
|
||||||
timesUs[i] = cueTimesUs.get(i);
|
|
||||||
offsets[i] = segmentStartPosition + 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) (segmentEndPosition - offsets[cuePointsSize - 1]);
|
|
||||||
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
|
|
||||||
cues = new SegmentIndex(sizeBytes, sizes, offsets, durationsUs, timesUs);
|
|
||||||
cueTimesUs = null;
|
|
||||||
cueClusterPositions = null;
|
|
||||||
|
|
||||||
prepared = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
|
|
||||||
private final SparseArray<FragmentedMp4Extractor> extractors;
|
private final SparseArray<FragmentedMp4Extractor> extractors;
|
||||||
private final Format[] formats;
|
private final SmoothStreamingFormat[] formats;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param baseUrl The base URL for the streams.
|
* @param baseUrl The base URL for the streams.
|
||||||
@ -94,23 +94,24 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
|
int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length;
|
||||||
formats = new Format[trackCount];
|
formats = new SmoothStreamingFormat[trackCount];
|
||||||
extractors = new SparseArray<FragmentedMp4Extractor>();
|
extractors = new SparseArray<FragmentedMp4Extractor>();
|
||||||
int maxWidth = 0;
|
int maxWidth = 0;
|
||||||
int maxHeight = 0;
|
int maxHeight = 0;
|
||||||
for (int i = 0; i < trackCount; i++) {
|
for (int i = 0; i < trackCount; i++) {
|
||||||
int trackIndex = trackIndices != null ? trackIndices[i] : i;
|
int trackIndex = trackIndices != null ? trackIndices[i] : i;
|
||||||
TrackElement trackElement = streamElement.tracks[trackIndex];
|
TrackElement trackElement = streamElement.tracks[trackIndex];
|
||||||
formats[i] = new Format(trackIndex, trackElement.mimeType, trackElement.maxWidth,
|
formats[i] = new SmoothStreamingFormat(String.valueOf(trackIndex), trackElement.mimeType,
|
||||||
trackElement.maxHeight, trackElement.numChannels, trackElement.sampleRate,
|
trackElement.maxWidth, trackElement.maxHeight, trackElement.numChannels,
|
||||||
trackElement.bitrate / 8);
|
trackElement.sampleRate, trackElement.bitrate, trackIndex);
|
||||||
maxWidth = Math.max(maxWidth, trackElement.maxWidth);
|
maxWidth = Math.max(maxWidth, trackElement.maxWidth);
|
||||||
maxHeight = Math.max(maxHeight, trackElement.maxHeight);
|
maxHeight = Math.max(maxHeight, trackElement.maxHeight);
|
||||||
|
|
||||||
MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex);
|
MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex);
|
||||||
int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO
|
int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO
|
||||||
: Track.TYPE_AUDIO;
|
: Track.TYPE_AUDIO;
|
||||||
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(true);
|
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
||||||
|
FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
|
||||||
extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat,
|
extractor.setTrack(new Track(trackIndex, trackType, streamElement.timeScale, mediaFormat,
|
||||||
trackEncryptionBoxes));
|
trackEncryptionBoxes));
|
||||||
if (protectionElement != null) {
|
if (protectionElement != null) {
|
||||||
@ -141,7 +142,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable(List<MediaChunk> queue) {
|
public void disable(List<? extends MediaChunk> queue) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,14 +156,14 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
long playbackPositionUs, ChunkOperationHolder out) {
|
long playbackPositionUs, ChunkOperationHolder out) {
|
||||||
evaluation.queueSize = queue.size();
|
evaluation.queueSize = queue.size();
|
||||||
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
|
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
|
||||||
Format selectedFormat = evaluation.format;
|
SmoothStreamingFormat selectedFormat = (SmoothStreamingFormat) evaluation.format;
|
||||||
out.queueSize = evaluation.queueSize;
|
out.queueSize = evaluation.queueSize;
|
||||||
|
|
||||||
if (selectedFormat == null) {
|
if (selectedFormat == null) {
|
||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
} else if (out.queueSize == queue.size() && out.chunk != null
|
} else if (out.queueSize == queue.size() && out.chunk != null
|
||||||
&& out.chunk.format.id == evaluation.format.id) {
|
&& out.chunk.format.id.equals(evaluation.format.id)) {
|
||||||
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
// We already have a chunk, and the evaluation hasn't changed either the format or the size
|
||||||
// of the queue. Do nothing.
|
// of the queue. Do nothing.
|
||||||
return;
|
return;
|
||||||
@ -181,11 +182,12 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
|
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
|
||||||
String requestUrl = streamElement.buildRequestUrl(selectedFormat.id, nextChunkIndex);
|
String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex,
|
||||||
|
nextChunkIndex);
|
||||||
Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
|
Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
|
||||||
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
|
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
|
||||||
extractors.get(selectedFormat.id), dataSource, nextChunkIndex, isLastChunk,
|
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
|
||||||
streamElement.getStartTimeUs(nextChunkIndex),
|
isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
|
||||||
isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
|
isLastChunk ? -1 : streamElement.getStartTimeUs(nextChunkIndex + 1), 0);
|
||||||
out.chunk = mediaChunk;
|
out.chunk = mediaChunk;
|
||||||
}
|
}
|
||||||
@ -195,6 +197,11 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChunkLoadError(Chunk chunk, Exception e) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
|
private static MediaFormat getMediaFormat(StreamElement streamElement, int trackIndex) {
|
||||||
TrackElement trackElement = streamElement.tracks[trackIndex];
|
TrackElement trackElement = streamElement.tracks[trackIndex];
|
||||||
String mimeType = trackElement.mimeType;
|
String mimeType = trackElement.mimeType;
|
||||||
@ -228,8 +235,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
|
DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey);
|
||||||
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
|
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
|
||||||
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
|
// To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs.
|
||||||
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, extractor,
|
return new Mp4MediaChunk(dataSource, dataSpec, formatInfo, trigger, chunkStartTimeUs,
|
||||||
chunkStartTimeUs, nextStartTimeUs, -chunkStartTimeUs, nextChunkIndex);
|
nextStartTimeUs, nextChunkIndex, extractor, false, -chunkStartTimeUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] getKeyId(byte[] initData) {
|
private static byte[] getKeyId(byte[] initData) {
|
||||||
@ -254,4 +261,16 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
data[secondPosition] = temp;
|
data[secondPosition] = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class SmoothStreamingFormat extends Format {
|
||||||
|
|
||||||
|
public final int trackIndex;
|
||||||
|
|
||||||
|
public SmoothStreamingFormat(String id, String mimeType, int width, int height,
|
||||||
|
int numChannels, int audioSamplingRate, int bitrate, int trackIndex) {
|
||||||
|
super(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate);
|
||||||
|
this.trackIndex = trackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
package com.google.android.exoplayer.smoothstreaming;
|
package com.google.android.exoplayer.smoothstreaming;
|
||||||
|
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,9 +195,7 @@ public class SmoothStreamingManifest {
|
|||||||
* @return The index of the corresponding chunk.
|
* @return The index of the corresponding chunk.
|
||||||
*/
|
*/
|
||||||
public int getChunkIndex(long timeUs) {
|
public int getChunkIndex(long timeUs) {
|
||||||
long time = (timeUs * timeScale) / 1000000L;
|
return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true);
|
||||||
int chunkIndex = Arrays.binarySearch(chunkStartTimes, time);
|
|
||||||
return chunkIndex < 0 ? -chunkIndex - 2 : chunkIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,6 +18,8 @@ package com.google.android.exoplayer.smoothstreaming;
|
|||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -56,7 +58,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) throws IOException, ParserException {
|
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||||
try {
|
try {
|
||||||
return parser.parse(stream, inputEncoding);
|
return parser.parse(stream, inputEncoding);
|
||||||
} catch (XmlPullParserException e) {
|
} catch (XmlPullParserException e) {
|
||||||
|
@ -16,8 +16,7 @@
|
|||||||
package com.google.android.exoplayer.text.ttml;
|
package com.google.android.exoplayer.text.ttml;
|
||||||
|
|
||||||
import com.google.android.exoplayer.text.Subtitle;
|
import com.google.android.exoplayer.text.Subtitle;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of a TTML subtitle.
|
* A representation of a TTML subtitle.
|
||||||
@ -41,8 +40,7 @@ public final class TtmlSubtitle implements Subtitle {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getNextEventTimeIndex(long timeUs) {
|
public int getNextEventTimeIndex(long timeUs) {
|
||||||
int index = Arrays.binarySearch(eventTimesUs, timeUs - startTimeUs);
|
int index = Util.binarySearchCeil(eventTimesUs, timeUs - startTimeUs, false, false);
|
||||||
index = index >= 0 ? index + 1 : ~index;
|
|
||||||
return index < eventTimesUs.length ? index : -1;
|
return index < eventTimesUs.length ? index : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream
|
|||||||
*/
|
*/
|
||||||
private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset,
|
private int read(ByteBuffer target, byte[] targetArray, int targetArrayOffset,
|
||||||
ReadHead readHead, int readLength) {
|
ReadHead readHead, int readLength) {
|
||||||
if (readHead.position == dataSpec.length) {
|
if (isEndOfStream()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength);
|
int bytesToRead = (int) Math.min(loadPosition - readHead.position, readLength);
|
||||||
|
@ -115,8 +115,8 @@ public class DefaultBandwidthMeter implements BandwidthMeter, TransferListener {
|
|||||||
float bytesPerSecond = accumulator * 1000 / elapsedMs;
|
float bytesPerSecond = accumulator * 1000 / elapsedMs;
|
||||||
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
|
slidingPercentile.addSample(computeWeight(accumulator), bytesPerSecond);
|
||||||
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
|
float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f);
|
||||||
bandwidthEstimate = bandwidthEstimateFloat == Float.NaN
|
bandwidthEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE
|
||||||
? NO_ESTIMATE : (long) bandwidthEstimateFloat;
|
: (long) bandwidthEstimateFloat;
|
||||||
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
|
notifyBandwidthSample(elapsedMs, accumulator, bandwidthEstimate);
|
||||||
}
|
}
|
||||||
streamCount--;
|
streamCount--;
|
||||||
|
@ -134,9 +134,8 @@ public interface Cache {
|
|||||||
* @param key The key of the data being requested.
|
* @param key The key of the data being requested.
|
||||||
* @param position The position of the data being requested.
|
* @param position The position of the data being requested.
|
||||||
* @return The {@link CacheSpan}. Or null if the cache entry is locked.
|
* @return The {@link CacheSpan}. Or null if the cache entry is locked.
|
||||||
* @throws InterruptedException
|
|
||||||
*/
|
*/
|
||||||
CacheSpan startReadWriteNonBlocking(String key, long position) throws InterruptedException;
|
CacheSpan startReadWriteNonBlocking(String key, long position);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains a cache file into which data can be written. Must only be called when holding a
|
* Obtains a cache file into which data can be written. Must only be called when holding a
|
||||||
@ -173,4 +172,14 @@ public interface Cache {
|
|||||||
*/
|
*/
|
||||||
void removeSpan(CacheSpan span);
|
void removeSpan(CacheSpan span);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries if a range is entirely available in the cache.
|
||||||
|
*
|
||||||
|
* @param key The cache key for the data.
|
||||||
|
* @param position The starting position of the data.
|
||||||
|
* @param length The length of the data.
|
||||||
|
* @return true if the data is available in the Cache otherwise false;
|
||||||
|
*/
|
||||||
|
boolean isCached(String key, long position, long length);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -109,26 +109,29 @@ public class SimpleCache implements Cache {
|
|||||||
public synchronized CacheSpan startReadWrite(String key, long position)
|
public synchronized CacheSpan startReadWrite(String key, long position)
|
||||||
throws InterruptedException {
|
throws InterruptedException {
|
||||||
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
|
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
|
||||||
// Wait until no-one holds a lock for the key.
|
while (true) {
|
||||||
while (lockedSpans.containsKey(key)) {
|
CacheSpan span = startReadWriteNonBlocking(lookupSpan);
|
||||||
wait();
|
if (span != null) {
|
||||||
|
return span;
|
||||||
|
} else {
|
||||||
|
// Write case, lock not available. We'll be woken up when a locked span is released (if the
|
||||||
|
// released lock is for the requested key then we'll be able to make progress) or when a
|
||||||
|
// span is added to the cache (if the span is for the requested key and covers the requested
|
||||||
|
// position, then we'll become a read and be able to make progress).
|
||||||
|
wait();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return getSpanningRegion(key, lookupSpan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position)
|
public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) {
|
||||||
throws InterruptedException {
|
return startReadWriteNonBlocking(CacheSpan.createLookup(key, position));
|
||||||
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
|
|
||||||
// Return null if key is locked
|
|
||||||
if (lockedSpans.containsKey(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return getSpanningRegion(key, lookupSpan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CacheSpan getSpanningRegion(String key, CacheSpan lookupSpan) {
|
private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) {
|
||||||
CacheSpan spanningRegion = getSpan(lookupSpan);
|
CacheSpan spanningRegion = getSpan(lookupSpan);
|
||||||
|
|
||||||
|
// Read case.
|
||||||
if (spanningRegion.isCached) {
|
if (spanningRegion.isCached) {
|
||||||
CacheSpan oldCacheSpan = spanningRegion;
|
CacheSpan oldCacheSpan = spanningRegion;
|
||||||
// Remove the old span from the in-memory representation.
|
// Remove the old span from the in-memory representation.
|
||||||
@ -139,10 +142,17 @@ public class SimpleCache implements Cache {
|
|||||||
// Add the updated span back into the in-memory representation.
|
// Add the updated span back into the in-memory representation.
|
||||||
spansForKey.add(spanningRegion);
|
spansForKey.add(spanningRegion);
|
||||||
notifySpanTouched(oldCacheSpan, spanningRegion);
|
notifySpanTouched(oldCacheSpan, spanningRegion);
|
||||||
} else {
|
return spanningRegion;
|
||||||
lockedSpans.put(key, spanningRegion);
|
|
||||||
}
|
}
|
||||||
return spanningRegion;
|
|
||||||
|
// Write case, lock available.
|
||||||
|
if (!lockedSpans.containsKey(lookupSpan.key)) {
|
||||||
|
lockedSpans.put(lookupSpan.key, spanningRegion);
|
||||||
|
return spanningRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write case, lock not available.
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -173,6 +183,7 @@ public class SimpleCache implements Cache {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addSpan(span);
|
addSpan(span);
|
||||||
|
notifyAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -330,4 +341,41 @@ public class SimpleCache implements Cache {
|
|||||||
evictor.onSpanTouched(this, oldSpan, newSpan);
|
evictor.onSpanTouched(this, oldSpan, newSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean isCached(String key, long position, long length) {
|
||||||
|
TreeSet<CacheSpan> entries = cachedSpans.get(key);
|
||||||
|
if (entries == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CacheSpan lookupSpan = CacheSpan.createLookup(key, position);
|
||||||
|
CacheSpan floorSpan = entries.floor(lookupSpan);
|
||||||
|
if (floorSpan == null || floorSpan.position + floorSpan.length <= position) {
|
||||||
|
// We don't have a span covering the start of the queried region.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long queryEndPosition = position + length;
|
||||||
|
long currentEndPosition = floorSpan.position + floorSpan.length;
|
||||||
|
if (currentEndPosition >= queryEndPosition) {
|
||||||
|
// floorSpan covers the queried region.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Iterator<CacheSpan> iterator = entries.tailSet(floorSpan, false).iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
CacheSpan next = iterator.next();
|
||||||
|
if (next.position > currentEndPosition) {
|
||||||
|
// There's a hole in the cache within the queried region.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// We expect currentEndPosition to always equal (next.position + next.length), but
|
||||||
|
// perform a max check anyway to guard against the existence of overlapping spans.
|
||||||
|
currentEndPosition = Math.max(currentEndPosition, next.position + next.length);
|
||||||
|
if (currentEndPosition >= queryEndPosition) {
|
||||||
|
// We've found spans covering the queried region.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We ran out of spans before covering the queried region.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer.util;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -84,14 +85,15 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
|||||||
protected final T doInBackground(String... data) {
|
protected final T doInBackground(String... data) {
|
||||||
try {
|
try {
|
||||||
contentId = data.length > 1 ? data[1] : null;
|
contentId = data.length > 1 ? data[1] : null;
|
||||||
URL url = new URL(data[0]);
|
String urlString = data[0];
|
||||||
String inputEncoding = null;
|
String inputEncoding = null;
|
||||||
InputStream inputStream = null;
|
InputStream inputStream = null;
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = configureHttpConnection(url);
|
Uri baseUrl = Util.parseBaseUri(urlString);
|
||||||
|
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
|
||||||
inputStream = connection.getInputStream();
|
inputStream = connection.getInputStream();
|
||||||
inputEncoding = connection.getContentEncoding();
|
inputEncoding = connection.getContentEncoding();
|
||||||
return parse(inputStream, inputEncoding, contentId);
|
return parse(inputStream, inputEncoding, contentId, baseUrl);
|
||||||
} finally {
|
} finally {
|
||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
inputStream.close();
|
inputStream.close();
|
||||||
@ -119,11 +121,13 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
|||||||
* @param stream The input stream to read.
|
* @param stream The input stream to read.
|
||||||
* @param inputEncoding The encoding of the input stream.
|
* @param inputEncoding The encoding of the input stream.
|
||||||
* @param contentId The content id of the media.
|
* @param contentId The content id of the media.
|
||||||
|
* @param baseUrl Required where the manifest contains urls that are relative to a base url. May
|
||||||
|
* be null where this is not the case.
|
||||||
* @throws IOException If an error occurred loading the data.
|
* @throws IOException If an error occurred loading the data.
|
||||||
* @throws ParserException If an error occurred parsing the loaded data.
|
* @throws ParserException If an error occurred parsing the loaded data.
|
||||||
*/
|
*/
|
||||||
protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws
|
protected abstract T parse(InputStream stream, String inputEncoding, String contentId,
|
||||||
IOException, ParserException;
|
Uri baseUrl) throws IOException, ParserException;
|
||||||
|
|
||||||
private HttpURLConnection configureHttpConnection(URL url) throws IOException {
|
private HttpURLConnection configureHttpConnection(URL url) throws IOException {
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
@ -20,17 +20,39 @@ package com.google.android.exoplayer.util;
|
|||||||
*/
|
*/
|
||||||
public class MimeTypes {
|
public class MimeTypes {
|
||||||
|
|
||||||
public static final String VIDEO_MP4 = "video/mp4";
|
public static final String BASE_TYPE_VIDEO = "video";
|
||||||
public static final String VIDEO_WEBM = "video/webm";
|
public static final String BASE_TYPE_AUDIO = "audio";
|
||||||
public static final String VIDEO_H264 = "video/avc";
|
public static final String BASE_TYPE_TEXT = "text";
|
||||||
public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9";
|
public static final String BASE_TYPE_APPLICATION = "application";
|
||||||
public static final String AUDIO_MP4 = "audio/mp4";
|
|
||||||
public static final String AUDIO_AAC = "audio/mp4a-latm";
|
public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4";
|
||||||
public static final String TEXT_VTT = "text/vtt";
|
public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm";
|
||||||
public static final String APPLICATION_TTML = "application/ttml+xml";
|
public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
|
||||||
|
public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9";
|
||||||
|
|
||||||
|
public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4";
|
||||||
|
public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm";
|
||||||
|
|
||||||
|
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
|
||||||
|
|
||||||
|
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
|
||||||
|
|
||||||
private MimeTypes() {}
|
private MimeTypes() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the top-level type of {@code mimeType}.
|
||||||
|
*
|
||||||
|
* @param mimeType The mimeType whose top-level type is required.
|
||||||
|
* @return The top-level type.
|
||||||
|
*/
|
||||||
|
public static String getTopLevelType(String mimeType) {
|
||||||
|
int indexOfSlash = mimeType.indexOf('/');
|
||||||
|
if (indexOfSlash == -1) {
|
||||||
|
throw new IllegalArgumentException("Invalid mime type: " + mimeType);
|
||||||
|
}
|
||||||
|
return mimeType.substring(0, indexOfSlash);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the top-level type of {@code mimeType} is audio.
|
* Whether the top-level type of {@code mimeType} is audio.
|
||||||
*
|
*
|
||||||
@ -38,7 +60,7 @@ public class MimeTypes {
|
|||||||
* @return Whether the top level type is audio.
|
* @return Whether the top level type is audio.
|
||||||
*/
|
*/
|
||||||
public static boolean isAudio(String mimeType) {
|
public static boolean isAudio(String mimeType) {
|
||||||
return mimeType.startsWith("audio/");
|
return getTopLevelType(mimeType).equals(BASE_TYPE_AUDIO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +70,7 @@ public class MimeTypes {
|
|||||||
* @return Whether the top level type is video.
|
* @return Whether the top level type is video.
|
||||||
*/
|
*/
|
||||||
public static boolean isVideo(String mimeType) {
|
public static boolean isVideo(String mimeType) {
|
||||||
return mimeType.startsWith("video/");
|
return getTopLevelType(mimeType).equals(BASE_TYPE_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,7 +80,27 @@ public class MimeTypes {
|
|||||||
* @return Whether the top level type is text.
|
* @return Whether the top level type is text.
|
||||||
*/
|
*/
|
||||||
public static boolean isText(String mimeType) {
|
public static boolean isText(String mimeType) {
|
||||||
return mimeType.startsWith("text/");
|
return getTopLevelType(mimeType).equals(BASE_TYPE_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the top-level type of {@code mimeType} is application.
|
||||||
|
*
|
||||||
|
* @param mimeType The mimeType to test.
|
||||||
|
* @return Whether the top level type is application.
|
||||||
|
*/
|
||||||
|
public static boolean isApplication(String mimeType) {
|
||||||
|
return getTopLevelType(mimeType).equals(BASE_TYPE_APPLICATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the mimeType is {@link #APPLICATION_TTML}.
|
||||||
|
*
|
||||||
|
* @param mimeType The mimeType to test.
|
||||||
|
* @return Whether the mimeType is {@link #APPLICATION_TTML}.
|
||||||
|
*/
|
||||||
|
public static boolean isTtml(String mimeType) {
|
||||||
|
return mimeType.equals(APPLICATION_TTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,13 @@ package com.google.android.exoplayer.util;
|
|||||||
|
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@ -112,4 +117,99 @@ public final class Util {
|
|||||||
return text == null ? null : text.toLowerCase(Locale.US);
|
return text == null ? null : text.toLowerCase(Locale.US);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final
|
||||||
|
* forward slash.
|
||||||
|
*
|
||||||
|
* @param uriString An RFC 2396-compliant, encoded uri.
|
||||||
|
* @return The parsed base uri.
|
||||||
|
*/
|
||||||
|
public static Uri parseBaseUri(String uriString) {
|
||||||
|
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the largest value in an array that is less than (or optionally equal to)
|
||||||
|
* a specified key.
|
||||||
|
* <p>
|
||||||
|
* The search is performed using a binary search algorithm, and so the array must be sorted.
|
||||||
|
*
|
||||||
|
* @param a The array to search.
|
||||||
|
* @param key The key being searched for.
|
||||||
|
* @param inclusive If the key is present in the array, whether to return the corresponding index.
|
||||||
|
* If false then the returned index corresponds to the largest value in the array that is
|
||||||
|
* strictly less than the key.
|
||||||
|
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
|
||||||
|
* the smallest value in the array. If false then -1 will be returned.
|
||||||
|
*/
|
||||||
|
public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) {
|
||||||
|
int index = Arrays.binarySearch(a, key);
|
||||||
|
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
|
||||||
|
return stayInBounds ? Math.max(0, index) : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the smallest value in an array that is greater than (or optionally equal
|
||||||
|
* to) a specified key.
|
||||||
|
* <p>
|
||||||
|
* The search is performed using a binary search algorithm, and so the array must be sorted.
|
||||||
|
*
|
||||||
|
* @param a The array to search.
|
||||||
|
* @param key The key being searched for.
|
||||||
|
* @param inclusive If the key is present in the array, whether to return the corresponding index.
|
||||||
|
* If false then the returned index corresponds to the smallest value in the array that is
|
||||||
|
* strictly greater than the key.
|
||||||
|
* @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the
|
||||||
|
* key is greater than the largest value in the array. If false then {@code a.length} will be
|
||||||
|
* returned.
|
||||||
|
*/
|
||||||
|
public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) {
|
||||||
|
int index = Arrays.binarySearch(a, key);
|
||||||
|
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
|
||||||
|
return stayInBounds ? Math.min(a.length - 1, index) : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the largest value in an list that is less than (or optionally equal to)
|
||||||
|
* a specified key.
|
||||||
|
* <p>
|
||||||
|
* The search is performed using a binary search algorithm, and so the list must be sorted.
|
||||||
|
*
|
||||||
|
* @param list The list to search.
|
||||||
|
* @param key The key being searched for.
|
||||||
|
* @param inclusive If the key is present in the list, whether to return the corresponding index.
|
||||||
|
* If false then the returned index corresponds to the largest value in the list that is
|
||||||
|
* strictly less than the key.
|
||||||
|
* @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
|
||||||
|
* the smallest value in the list. If false then -1 will be returned.
|
||||||
|
*/
|
||||||
|
public static<T> int binarySearchFloor(List<? extends Comparable<? super T>> list, T key,
|
||||||
|
boolean inclusive, boolean stayInBounds) {
|
||||||
|
int index = Collections.binarySearch(list, key);
|
||||||
|
index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
|
||||||
|
return stayInBounds ? Math.max(0, index) : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the smallest value in an list that is greater than (or optionally equal
|
||||||
|
* to) a specified key.
|
||||||
|
* <p>
|
||||||
|
* The search is performed using a binary search algorithm, and so the list must be sorted.
|
||||||
|
*
|
||||||
|
* @param list The list to search.
|
||||||
|
* @param key The key being searched for.
|
||||||
|
* @param inclusive If the key is present in the list, whether to return the corresponding index.
|
||||||
|
* If false then the returned index corresponds to the smallest value in the list that is
|
||||||
|
* strictly greater than the key.
|
||||||
|
* @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that
|
||||||
|
* the key is greater than the largest value in the list. If false then {@code list.size()}
|
||||||
|
* will be returned.
|
||||||
|
*/
|
||||||
|
public static<T> int binarySearchCeil(List<? extends Comparable<? super T>> list, T key,
|
||||||
|
boolean inclusive, boolean stayInBounds) {
|
||||||
|
int index = Collections.binarySearch(list, key);
|
||||||
|
index = index < 0 ? ~index : (inclusive ? index : (index + 1));
|
||||||
|
return stayInBounds ? Math.min(list.size() - 1, index) : index;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user