1. Parse live attributes from SmoothStreaming manifest.
2. Common interface for manifest parsers. - This effectively moves the common interface from the Fetcher level (i.e. ManifestFetcher) to the Parser level (i.e. ManifestParser). - The motivation here is to allow the implementation of components that can work with a generic ManifestParser implementation.
This commit is contained in:
parent
d4e35358a1
commit
bf5ee6ff23
@ -31,7 +31,7 @@ import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
|||||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||||
import com.google.android.exoplayer.dash.mpd.Period;
|
import com.google.android.exoplayer.dash.mpd.Period;
|
||||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||||
import com.google.android.exoplayer.demo.DemoUtil;
|
import com.google.android.exoplayer.demo.DemoUtil;
|
||||||
@ -44,6 +44,7 @@ import com.google.android.exoplayer.upstream.BufferPool;
|
|||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.HttpDataSource;
|
import com.google.android.exoplayer.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
@ -94,7 +95,9 @@ public class DashVodRendererBuilder implements RendererBuilder,
|
|||||||
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this);
|
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||||
|
ManifestFetcher<MediaPresentationDescription> mpdFetcher =
|
||||||
|
new ManifestFetcher<MediaPresentationDescription>(parser, this);
|
||||||
mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId);
|
mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,13 +35,14 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
|
|||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
|
||||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||||
import com.google.android.exoplayer.text.ttml.TtmlParser;
|
import com.google.android.exoplayer.text.ttml.TtmlParser;
|
||||||
import com.google.android.exoplayer.upstream.BufferPool;
|
import com.google.android.exoplayer.upstream.BufferPool;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.HttpDataSource;
|
import com.google.android.exoplayer.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
@ -87,8 +88,11 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this);
|
|
||||||
mpdFetcher.execute(url + "/Manifest", contentId);
|
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||||
|
ManifestFetcher<SmoothStreamingManifest> manifestFetcher =
|
||||||
|
new ManifestFetcher<SmoothStreamingManifest>(parser, this);
|
||||||
|
manifestFetcher.execute(url + "/Manifest", contentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -151,7 +155,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest,
|
||||||
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
||||||
new AdaptiveEvaluator(bandwidthMeter));
|
new AdaptiveEvaluator(bandwidthMeter));
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
@ -178,7 +182,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
for (int i = 0; i < manifest.streamElements.length; i++) {
|
for (int i = 0; i < manifest.streamElements.length; i++) {
|
||||||
if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
|
if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
|
||||||
audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name;
|
audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name;
|
||||||
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
|
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(manifest,
|
||||||
i, new int[] {0}, audioDataSource, audioFormatEvaluator);
|
i, new int[] {0}, audioDataSource, audioFormatEvaluator);
|
||||||
audioStreamElementCount++;
|
audioStreamElementCount++;
|
||||||
}
|
}
|
||||||
@ -208,7 +212,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
|||||||
for (int i = 0; i < manifest.streamElements.length; i++) {
|
for (int i = 0; i < manifest.streamElements.length; i++) {
|
||||||
if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
|
if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
|
||||||
textTrackNames[textStreamElementCount] = manifest.streamElements[i].language;
|
textTrackNames[textStreamElementCount] = manifest.streamElements[i].language;
|
||||||
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
|
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifest,
|
||||||
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator);
|
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator);
|
||||||
textStreamElementCount++;
|
textStreamElementCount++;
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
|||||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||||
import com.google.android.exoplayer.dash.mpd.Period;
|
import com.google.android.exoplayer.dash.mpd.Period;
|
||||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||||
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
|
import com.google.android.exoplayer.demo.simple.SimplePlayerActivity.RendererBuilder;
|
||||||
@ -38,6 +38,7 @@ import com.google.android.exoplayer.upstream.BufferPool;
|
|||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.HttpDataSource;
|
import com.google.android.exoplayer.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
@ -74,7 +75,9 @@ import java.util.ArrayList;
|
|||||||
@Override
|
@Override
|
||||||
public void buildRenderers(RendererBuilderCallback callback) {
|
public void buildRenderers(RendererBuilderCallback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
MediaPresentationDescriptionFetcher mpdFetcher = new MediaPresentationDescriptionFetcher(this);
|
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||||
|
ManifestFetcher<MediaPresentationDescription> mpdFetcher =
|
||||||
|
new ManifestFetcher<MediaPresentationDescription>(parser, this);
|
||||||
mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId);
|
mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,11 +31,12 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
|
|||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestFetcher;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
|
||||||
import com.google.android.exoplayer.upstream.BufferPool;
|
import com.google.android.exoplayer.upstream.BufferPool;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer.upstream.HttpDataSource;
|
import com.google.android.exoplayer.upstream.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
@ -71,8 +72,10 @@ import java.util.ArrayList;
|
|||||||
@Override
|
@Override
|
||||||
public void buildRenderers(RendererBuilderCallback callback) {
|
public void buildRenderers(RendererBuilderCallback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
SmoothStreamingManifestFetcher mpdFetcher = new SmoothStreamingManifestFetcher(this);
|
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||||
mpdFetcher.execute(url + "/Manifest", contentId);
|
ManifestFetcher<SmoothStreamingManifest> manifestFetcher =
|
||||||
|
new ManifestFetcher<SmoothStreamingManifest>(parser, this);
|
||||||
|
manifestFetcher.execute(url + "/Manifest", contentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -116,9 +119,8 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
// Build the video renderer.
|
// Build the video renderer.
|
||||||
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex,
|
||||||
videoStreamElementIndex, videoTrackIndices, videoDataSource,
|
videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter));
|
||||||
new AdaptiveEvaluator(bandwidthMeter));
|
|
||||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
|
||||||
@ -126,9 +128,8 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
// Build the audio renderer.
|
// Build the audio renderer.
|
||||||
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
|
||||||
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
|
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifest, audioStreamElementIndex,
|
||||||
audioStreamElementIndex, new int[] {0}, audioDataSource,
|
new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator());
|
||||||
new FormatEvaluator.FixedEvaluator());
|
|
||||||
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
|
||||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
|
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(
|
||||||
|
@ -25,6 +25,11 @@ public final class C {
|
|||||||
*/
|
*/
|
||||||
public static final int LENGTH_UNBOUNDED = -1;
|
public static final int LENGTH_UNBOUNDED = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the UTF-8 charset.
|
||||||
|
*/
|
||||||
|
public static final String UTF8_NAME = "UTF-8";
|
||||||
|
|
||||||
private C() {}
|
private C() {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,64 +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.ParserException;
|
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A concrete implementation of {@link ManifestFetcher} for loading DASH manifests.
|
|
||||||
* <p>
|
|
||||||
* This class is provided for convenience, however it is expected that most applications will
|
|
||||||
* contain their own mechanisms for making asynchronous network requests and parsing the response.
|
|
||||||
* In such cases it is recommended that application developers use their existing solution rather
|
|
||||||
* than this one.
|
|
||||||
*/
|
|
||||||
public final class MediaPresentationDescriptionFetcher extends
|
|
||||||
ManifestFetcher<MediaPresentationDescription> {
|
|
||||||
|
|
||||||
private final MediaPresentationDescriptionParser parser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callback The callback to provide with the parsed manifest (or error).
|
|
||||||
*/
|
|
||||||
public MediaPresentationDescriptionFetcher(
|
|
||||||
ManifestCallback<MediaPresentationDescription> callback) {
|
|
||||||
super(callback);
|
|
||||||
parser = new MediaPresentationDescriptionParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callback The callback to provide with the parsed manifest (or error).
|
|
||||||
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
|
|
||||||
*/
|
|
||||||
public MediaPresentationDescriptionFetcher(
|
|
||||||
ManifestCallback<MediaPresentationDescription> callback, int timeoutMillis) {
|
|
||||||
super(callback, timeoutMillis);
|
|
||||||
parser = new MediaPresentationDescriptionParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
|
|
||||||
String contentId, Uri baseUrl) throws IOException, ParserException {
|
|
||||||
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -22,7 +22,9 @@ 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.SegmentTimelineElement;
|
||||||
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
|
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.ManifestParser;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@ -34,29 +36,15 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.GregorianCalendar;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A parser of media presentation description files.
|
* A parser of media presentation description files.
|
||||||
*/
|
*/
|
||||||
public class MediaPresentationDescriptionParser extends DefaultHandler {
|
public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||||
|
implements ManifestParser<MediaPresentationDescription> {
|
||||||
// Note: Does not support the date part of ISO 8601
|
|
||||||
private static final Pattern DURATION =
|
|
||||||
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
|
|
||||||
|
|
||||||
private static final Pattern DATE_TIME_PATTERN =
|
|
||||||
Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
|
|
||||||
+ "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?"
|
|
||||||
+ "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
|
|
||||||
|
|
||||||
private final XmlPullParserFactory xmlParserFactory;
|
private final XmlPullParserFactory xmlParserFactory;
|
||||||
|
|
||||||
@ -70,19 +58,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
|
|
||||||
// MPD parsing.
|
// MPD parsing.
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Parses a manifest from the provided {@link InputStream}.
|
public MediaPresentationDescription parse(InputStream inputStream, String inputEncoding,
|
||||||
*
|
String contentId, Uri baseUrl) throws IOException, ParserException {
|
||||||
* @param inputStream The stream from which to parse the manifest.
|
|
||||||
* @param inputEncoding The encoding of the input.
|
|
||||||
* @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.
|
|
||||||
* @throws IOException If a problem occurred reading from the stream.
|
|
||||||
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
|
|
||||||
*/
|
|
||||||
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
|
|
||||||
String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException {
|
|
||||||
try {
|
try {
|
||||||
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
XmlPullParser xpp = xmlParserFactory.newPullParser();
|
||||||
xpp.setInput(inputStream, inputEncoding);
|
xpp.setInput(inputStream, inputEncoding);
|
||||||
@ -102,12 +80,13 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
|
||||||
String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
|
String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
|
||||||
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
|
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
|
||||||
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
|
long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1);
|
||||||
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
|
long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1);
|
||||||
String typeString = xpp.getAttributeValue(null, "type");
|
String typeString = xpp.getAttributeValue(null, "type");
|
||||||
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
|
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
|
||||||
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
|
long minUpdateTimeMs = (dynamic) ? parseDuration(xpp, "minimumUpdatePeriod", -1) : -1;
|
||||||
long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1;
|
long timeShiftBufferDepthMs = (dynamic) ? parseDuration(xpp, "timeShiftBufferDepth", -1)
|
||||||
|
: -1;
|
||||||
UtcTimingElement utcTiming = null;
|
UtcTimingElement utcTiming = null;
|
||||||
|
|
||||||
List<Period> periods = new ArrayList<Period>();
|
List<Period> periods = new ArrayList<Period>();
|
||||||
@ -135,8 +114,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
|
|||||||
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
|
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
String id = xpp.getAttributeValue(null, "id");
|
String id = xpp.getAttributeValue(null, "id");
|
||||||
long startMs = parseDurationMs(xpp, "start", 0);
|
long startMs = parseDuration(xpp, "start", 0);
|
||||||
long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs);
|
long durationMs = parseDuration(xpp, "duration", mpdDurationMs);
|
||||||
SegmentBase segmentBase = null;
|
SegmentBase segmentBase = null;
|
||||||
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
|
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
|
||||||
do {
|
do {
|
||||||
@ -450,85 +429,25 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long parseDurationMs(XmlPullParser xpp, String name) {
|
private static long parseDuration(XmlPullParser xpp, String name, long defaultValue) {
|
||||||
return parseDurationMs(xpp, name, -1);
|
String value = xpp.getAttributeValue(null, name);
|
||||||
|
if (value == null) {
|
||||||
|
return defaultValue;
|
||||||
|
} else {
|
||||||
|
return Util.parseXsDuration(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
|
private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
|
||||||
throws ParseException {
|
throws ParseException {
|
||||||
String value = xpp.getAttributeValue(null, name);
|
String value = xpp.getAttributeValue(null, name);
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
} else {
|
} else {
|
||||||
return parseDateTime(value);
|
return Util.parseXsDateTime(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VisibleForTesting
|
|
||||||
static long parseDateTime(String value) throws ParseException {
|
|
||||||
Matcher matcher = DATE_TIME_PATTERN.matcher(value);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
throw new ParseException("Invalid date/time format: " + value, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int timezoneShift;
|
|
||||||
if (matcher.group(9) == null) {
|
|
||||||
// No time zone specified.
|
|
||||||
timezoneShift = 0;
|
|
||||||
} else if (matcher.group(9).equalsIgnoreCase("Z")) {
|
|
||||||
timezoneShift = 0;
|
|
||||||
} else {
|
|
||||||
timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60
|
|
||||||
+ Integer.valueOf(matcher.group(13))));
|
|
||||||
if (matcher.group(11).equals("-")) {
|
|
||||||
timezoneShift *= -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
|
||||||
|
|
||||||
dateTime.clear();
|
|
||||||
// Note: The month value is 0-based, hence the -1 on group(2)
|
|
||||||
dateTime.set(Integer.valueOf(matcher.group(1)),
|
|
||||||
Integer.valueOf(matcher.group(2)) - 1,
|
|
||||||
Integer.valueOf(matcher.group(3)),
|
|
||||||
Integer.valueOf(matcher.group(4)),
|
|
||||||
Integer.valueOf(matcher.group(5)),
|
|
||||||
Integer.valueOf(matcher.group(6)));
|
|
||||||
if (!TextUtils.isEmpty(matcher.group(8))) {
|
|
||||||
final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
|
|
||||||
// we care only for milliseconds, so movePointRight(3)
|
|
||||||
dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
long time = dateTime.getTimeInMillis();
|
|
||||||
if (timezoneShift != 0) {
|
|
||||||
time -= timezoneShift * 60000;
|
|
||||||
}
|
|
||||||
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
|
|
||||||
String value = xpp.getAttributeValue(null, name);
|
|
||||||
if (value != null) {
|
|
||||||
Matcher matcher = DURATION.matcher(value);
|
|
||||||
if (matcher.matches()) {
|
|
||||||
String hours = matcher.group(2);
|
|
||||||
double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
|
|
||||||
String minutes = matcher.group(4);
|
|
||||||
durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
|
|
||||||
String seconds = matcher.group(6);
|
|
||||||
durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
|
|
||||||
return (long) (durationSeconds * 1000);
|
|
||||||
} else {
|
|
||||||
return (long) (Double.parseDouble(value) * 3600 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
|
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer.dash.mpd;
|
package com.google.android.exoplayer.dash.mpd;
|
||||||
|
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
@ -47,15 +48,10 @@ public final class RangedUri {
|
|||||||
/**
|
/**
|
||||||
* Constructs an ranged uri.
|
* Constructs an ranged uri.
|
||||||
* <p>
|
* <p>
|
||||||
* The uri is built according to the following rules:
|
* See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and
|
||||||
* <ul>
|
* {@code stringUri} are merged.
|
||||||
* <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 baseUri A uri that can form the base of the uri defined by the instance.
|
||||||
* @param stringUri A relative or absolute uri in string form.
|
* @param stringUri A relative or absolute uri in string form.
|
||||||
* @param start The (zero based) index of the first byte of the range.
|
* @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.
|
* @param length The length of the range, or -1 to indicate that the range is unbounded.
|
||||||
@ -74,14 +70,7 @@ public final class RangedUri {
|
|||||||
* @return The {@link Uri} represented by the instance.
|
* @return The {@link Uri} represented by the instance.
|
||||||
*/
|
*/
|
||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
if (stringUri == null) {
|
return Util.getMergedUri(baseUri, stringUri);
|
||||||
return baseUri;
|
|
||||||
}
|
|
||||||
Uri uri = Uri.parse(stringUri);
|
|
||||||
if (!uri.isAbsolute() && baseUri != null) {
|
|
||||||
uri = Uri.withAppendedPath(baseUri, stringUri);
|
|
||||||
}
|
|
||||||
return uri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.parser.webm;
|
package com.google.android.exoplayer.parser.webm;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
|
|
||||||
@ -210,7 +211,7 @@ import java.util.Stack;
|
|||||||
if (stringResult != READ_RESULT_CONTINUE) {
|
if (stringResult != READ_RESULT_CONTINUE) {
|
||||||
return stringResult;
|
return stringResult;
|
||||||
}
|
}
|
||||||
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
|
String stringValue = new String(stringBytes, Charset.forName(C.UTF8_NAME));
|
||||||
stringBytes = null;
|
stringBytes = null;
|
||||||
eventHandler.onStringElement(elementId, stringValue);
|
eventHandler.onStringElement(elementId, stringValue);
|
||||||
prepareForNextElement();
|
prepareForNextElement();
|
||||||
|
@ -53,7 +53,6 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
private static final int INITIALIZATION_VECTOR_SIZE = 8;
|
private static final int INITIALIZATION_VECTOR_SIZE = 8;
|
||||||
|
|
||||||
private final String baseUrl;
|
|
||||||
private final StreamElement streamElement;
|
private final StreamElement streamElement;
|
||||||
private final TrackInfo trackInfo;
|
private final TrackInfo trackInfo;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
@ -67,7 +66,6 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
private final SmoothStreamingFormat[] formats;
|
private final SmoothStreamingFormat[] formats;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param baseUrl The base URL for the streams.
|
|
||||||
* @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}.
|
* @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}.
|
||||||
* @param streamElementIndex The index of the stream element in the manifest to be provided by
|
* @param streamElementIndex The index of the stream element in the manifest to be provided by
|
||||||
* the source.
|
* the source.
|
||||||
@ -76,10 +74,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||||
* @param formatEvaluator Selects from the available formats.
|
* @param formatEvaluator Selects from the available formats.
|
||||||
*/
|
*/
|
||||||
public SmoothStreamingChunkSource(String baseUrl, SmoothStreamingManifest manifest,
|
public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex,
|
||||||
int streamElementIndex, int[] trackIndices, DataSource dataSource,
|
int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
|
||||||
FormatEvaluator formatEvaluator) {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.streamElement = manifest.streamElements[streamElementIndex];
|
this.streamElement = manifest.streamElements[streamElementIndex];
|
||||||
this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.getDurationUs());
|
this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.getDurationUs());
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
@ -113,7 +109,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
: Track.TYPE_AUDIO;
|
: Track.TYPE_AUDIO;
|
||||||
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
||||||
FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
|
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) {
|
||||||
extractor.putPsshInfo(protectionElement.uuid, protectionElement.data);
|
extractor.putPsshInfo(protectionElement.uuid, protectionElement.data);
|
||||||
@ -183,9 +179,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
|
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
|
||||||
String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex,
|
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, nextChunkIndex);
|
||||||
nextChunkIndex);
|
|
||||||
Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
|
|
||||||
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
|
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
|
||||||
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
|
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
|
||||||
isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
|
isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),
|
||||||
|
@ -15,9 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.smoothstreaming;
|
package com.google.android.exoplayer.smoothstreaming;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,32 +34,48 @@ public class SmoothStreamingManifest {
|
|||||||
|
|
||||||
public final int majorVersion;
|
public final int majorVersion;
|
||||||
public final int minorVersion;
|
public final int minorVersion;
|
||||||
public final long timeScale;
|
public final long timescale;
|
||||||
public final int lookAheadCount;
|
public final int lookAheadCount;
|
||||||
|
public final boolean isLive;
|
||||||
public final ProtectionElement protectionElement;
|
public final ProtectionElement protectionElement;
|
||||||
public final StreamElement[] streamElements;
|
public final StreamElement[] streamElements;
|
||||||
|
|
||||||
private final long duration;
|
private final long duration;
|
||||||
|
private final long dvrWindowLength;
|
||||||
|
|
||||||
public SmoothStreamingManifest(int majorVersion, int minorVersion, long timeScale, long duration,
|
public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration,
|
||||||
int lookAheadCount, ProtectionElement protectionElement, StreamElement[] streamElements) {
|
long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
|
||||||
|
StreamElement[] streamElements) {
|
||||||
this.majorVersion = majorVersion;
|
this.majorVersion = majorVersion;
|
||||||
this.minorVersion = minorVersion;
|
this.minorVersion = minorVersion;
|
||||||
this.timeScale = timeScale;
|
this.timescale = timescale;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
|
this.dvrWindowLength = dvrWindowLength;
|
||||||
this.lookAheadCount = lookAheadCount;
|
this.lookAheadCount = lookAheadCount;
|
||||||
|
this.isLive = isLive;
|
||||||
this.protectionElement = protectionElement;
|
this.protectionElement = protectionElement;
|
||||||
this.streamElements = streamElements;
|
this.streamElements = streamElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the duration of the media.
|
* Gets the duration of the media.
|
||||||
|
* <p>
|
||||||
|
* For a live presentation the duration may be an approximation of the eventual final duration,
|
||||||
|
* or 0 if an approximate duration is not known.
|
||||||
*
|
*
|
||||||
*
|
* @return The duration of the media in microseconds.
|
||||||
* @return The duration of the media, in microseconds.
|
|
||||||
*/
|
*/
|
||||||
public long getDurationUs() {
|
public long getDurationUs() {
|
||||||
return (duration * 1000000L) / timeScale;
|
return (duration * 1000000L) / timescale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the DVR window length, or 0 if no window length was specified.
|
||||||
|
*
|
||||||
|
* @return The duration of the DVR window in microseconds, or 0 if no window length was specified.
|
||||||
|
*/
|
||||||
|
public long getDvrWindowLengthUs() {
|
||||||
|
return (dvrWindowLength * 1000000L) / timescale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -155,10 +175,9 @@ public class SmoothStreamingManifest {
|
|||||||
|
|
||||||
public final int type;
|
public final int type;
|
||||||
public final String subType;
|
public final String subType;
|
||||||
public final long timeScale;
|
public final long timescale;
|
||||||
public final String name;
|
public final String name;
|
||||||
public final int qualityLevels;
|
public final int qualityLevels;
|
||||||
public final String url;
|
|
||||||
public final int maxWidth;
|
public final int maxWidth;
|
||||||
public final int maxHeight;
|
public final int maxHeight;
|
||||||
public final int displayWidth;
|
public final int displayWidth;
|
||||||
@ -167,24 +186,29 @@ public class SmoothStreamingManifest {
|
|||||||
public final TrackElement[] tracks;
|
public final TrackElement[] tracks;
|
||||||
public final int chunkCount;
|
public final int chunkCount;
|
||||||
|
|
||||||
private final long[] chunkStartTimes;
|
private final Uri baseUri;
|
||||||
|
private final String chunkTemplate;
|
||||||
|
|
||||||
public StreamElement(int type, String subType, long timeScale, String name,
|
private final List<Long> chunkStartTimes;
|
||||||
int qualityLevels, String url, int maxWidth, int maxHeight, int displayWidth,
|
|
||||||
int displayHeight, String language, TrackElement[] tracks, long[] chunkStartTimes) {
|
public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType,
|
||||||
|
long timescale, String name, int qualityLevels, int maxWidth, int maxHeight,
|
||||||
|
int displayWidth, int displayHeight, String language, TrackElement[] tracks,
|
||||||
|
List<Long> chunkStartTimes) {
|
||||||
|
this.baseUri = baseUri;
|
||||||
|
this.chunkTemplate = chunkTemplate;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.subType = subType;
|
this.subType = subType;
|
||||||
this.timeScale = timeScale;
|
this.timescale = timescale;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.qualityLevels = qualityLevels;
|
this.qualityLevels = qualityLevels;
|
||||||
this.url = url;
|
|
||||||
this.maxWidth = maxWidth;
|
this.maxWidth = maxWidth;
|
||||||
this.maxHeight = maxHeight;
|
this.maxHeight = maxHeight;
|
||||||
this.displayWidth = displayWidth;
|
this.displayWidth = displayWidth;
|
||||||
this.displayHeight = displayHeight;
|
this.displayHeight = displayHeight;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
this.tracks = tracks;
|
this.tracks = tracks;
|
||||||
this.chunkCount = chunkStartTimes.length;
|
this.chunkCount = chunkStartTimes.size();
|
||||||
this.chunkStartTimes = chunkStartTimes;
|
this.chunkStartTimes = chunkStartTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +219,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) {
|
||||||
return Util.binarySearchFloor(chunkStartTimes, (timeUs * timeScale) / 1000000L, true, true);
|
return Util.binarySearchFloor(chunkStartTimes, (timeUs * timescale) / 1000000L, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -205,22 +229,24 @@ public class SmoothStreamingManifest {
|
|||||||
* @return The start time of the chunk, in microseconds.
|
* @return The start time of the chunk, in microseconds.
|
||||||
*/
|
*/
|
||||||
public long getStartTimeUs(int chunkIndex) {
|
public long getStartTimeUs(int chunkIndex) {
|
||||||
return (chunkStartTimes[chunkIndex] * 1000000L) / timeScale;
|
return (chunkStartTimes.get(chunkIndex) * 1000000L) / timescale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a URL for requesting the specified chunk of the specified track.
|
* Builds a uri for requesting the specified chunk of the specified track.
|
||||||
*
|
*
|
||||||
* @param track The index of the track for which to build the URL.
|
* @param track The index of the track for which to build the URL.
|
||||||
* @param chunkIndex The index of the chunk for which to build the URL.
|
* @param chunkIndex The index of the chunk for which to build the URL.
|
||||||
* @return The request URL.
|
* @return The request uri.
|
||||||
*/
|
*/
|
||||||
public String buildRequestUrl(int track, int chunkIndex) {
|
public Uri buildRequestUri(int track, int chunkIndex) {
|
||||||
assert (tracks != null);
|
Assertions.checkState(tracks != null);
|
||||||
assert (chunkStartTimes != null);
|
Assertions.checkState(chunkStartTimes != null);
|
||||||
assert (chunkIndex < chunkStartTimes.length);
|
Assertions.checkState(chunkIndex < chunkStartTimes.size());
|
||||||
return url.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
|
String chunkUrl = chunkTemplate
|
||||||
.replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes[chunkIndex]));
|
.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
|
||||||
|
.replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes.get(chunkIndex)));
|
||||||
|
return baseUri.buildUpon().appendEncodedPath(chunkUrl).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,63 +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.smoothstreaming;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer.ParserException;
|
|
||||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A concrete implementation of {@link ManifestFetcher} for loading SmoothStreaming
|
|
||||||
* manifests.
|
|
||||||
* <p>
|
|
||||||
* This class is provided for convenience, however it is expected that most applications will
|
|
||||||
* contain their own mechanisms for making asynchronous network requests and parsing the response.
|
|
||||||
* In such cases it is recommended that application developers use their existing solution rather
|
|
||||||
* than this one.
|
|
||||||
*/
|
|
||||||
public final class SmoothStreamingManifestFetcher extends ManifestFetcher<SmoothStreamingManifest> {
|
|
||||||
|
|
||||||
private final SmoothStreamingManifestParser parser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callback The callback to provide with the parsed manifest (or error).
|
|
||||||
*/
|
|
||||||
public SmoothStreamingManifestFetcher(ManifestCallback<SmoothStreamingManifest> callback) {
|
|
||||||
super(callback);
|
|
||||||
parser = new SmoothStreamingManifestParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callback The callback to provide with the parsed manifest (or error).
|
|
||||||
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
|
|
||||||
*/
|
|
||||||
public SmoothStreamingManifestFetcher(ManifestCallback<SmoothStreamingManifest> callback,
|
|
||||||
int timeoutMillis) {
|
|
||||||
super(callback, timeoutMillis);
|
|
||||||
parser = new SmoothStreamingManifestParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
|
|
||||||
String contentId, Uri baseUrl) throws IOException, ParserException {
|
|
||||||
return parser.parse(stream, inputEncoding);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -21,7 +21,9 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre
|
|||||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
import com.google.android.exoplayer.util.Assertions;
|
import com.google.android.exoplayer.util.Assertions;
|
||||||
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
|
import com.google.android.exoplayer.util.ManifestParser;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -41,7 +44,7 @@ import java.util.UUID;
|
|||||||
* @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
|
* @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
|
||||||
* IIS Smooth Streaming Client Manifest Format</a>
|
* IIS Smooth Streaming Client Manifest Format</a>
|
||||||
*/
|
*/
|
||||||
public class SmoothStreamingManifestParser {
|
public class SmoothStreamingManifestParser implements ManifestParser<SmoothStreamingManifest> {
|
||||||
|
|
||||||
private final XmlPullParserFactory xmlParserFactory;
|
private final XmlPullParserFactory xmlParserFactory;
|
||||||
|
|
||||||
@ -53,21 +56,13 @@ public class SmoothStreamingManifestParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Parses a manifest from the provided {@link InputStream}.
|
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding,
|
||||||
*
|
String contentId, Uri baseUri) throws IOException, ParserException {
|
||||||
* @param inputStream The stream from which to parse the manifest.
|
|
||||||
* @param inputEncoding The encoding of the input.
|
|
||||||
* @return The parsed manifest.
|
|
||||||
* @throws IOException If a problem occurred reading from the stream.
|
|
||||||
* @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest.
|
|
||||||
*/
|
|
||||||
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
|
|
||||||
IOException, ParserException {
|
|
||||||
try {
|
try {
|
||||||
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
|
||||||
xmlParser.setInput(inputStream, inputEncoding);
|
xmlParser.setInput(inputStream, inputEncoding);
|
||||||
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
|
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, baseUri);
|
||||||
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
|
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
|
||||||
} catch (XmlPullParserException e) {
|
} catch (XmlPullParserException e) {
|
||||||
throw new ParserException(e);
|
throw new ParserException(e);
|
||||||
@ -90,14 +85,16 @@ public class SmoothStreamingManifestParser {
|
|||||||
*/
|
*/
|
||||||
private static abstract class ElementParser {
|
private static abstract class ElementParser {
|
||||||
|
|
||||||
|
private final Uri baseUri;
|
||||||
private final String tag;
|
private final String tag;
|
||||||
|
|
||||||
private final ElementParser parent;
|
private final ElementParser parent;
|
||||||
private final List<Pair<String, Object>> normalizedAttributes;
|
private final List<Pair<String, Object>> normalizedAttributes;
|
||||||
|
|
||||||
public ElementParser(String tag, ElementParser parent) {
|
public ElementParser(ElementParser parent, Uri baseUri, String tag) {
|
||||||
this.tag = tag;
|
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
this.baseUri = baseUri;
|
||||||
|
this.tag = tag;
|
||||||
this.normalizedAttributes = new LinkedList<Pair<String, Object>>();
|
this.normalizedAttributes = new LinkedList<Pair<String, Object>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +117,7 @@ public class SmoothStreamingManifestParser {
|
|||||||
} else if (handleChildInline(tagName)) {
|
} else if (handleChildInline(tagName)) {
|
||||||
parseStartTag(xmlParser);
|
parseStartTag(xmlParser);
|
||||||
} else {
|
} else {
|
||||||
ElementParser childElementParser = newChildParser(this, tagName);
|
ElementParser childElementParser = newChildParser(this, tagName, baseUri);
|
||||||
if (childElementParser == null) {
|
if (childElementParser == null) {
|
||||||
skippingElementDepth = 1;
|
skippingElementDepth = 1;
|
||||||
} else {
|
} else {
|
||||||
@ -157,13 +154,13 @@ public class SmoothStreamingManifestParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ElementParser newChildParser(ElementParser parent, String name) {
|
private ElementParser newChildParser(ElementParser parent, String name, Uri baseUri) {
|
||||||
if (TrackElementParser.TAG.equals(name)) {
|
if (TrackElementParser.TAG.equals(name)) {
|
||||||
return new TrackElementParser(parent);
|
return new TrackElementParser(parent, baseUri);
|
||||||
} else if (ProtectionElementParser.TAG.equals(name)) {
|
} else if (ProtectionElementParser.TAG.equals(name)) {
|
||||||
return new ProtectionElementParser(parent);
|
return new ProtectionElementParser(parent, baseUri);
|
||||||
} else if (StreamElementParser.TAG.equals(name)) {
|
} else if (StreamElementParser.TAG.equals(name)) {
|
||||||
return new StreamElementParser(parent);
|
return new StreamElementParser(parent, baseUri);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -308,6 +305,15 @@ public class SmoothStreamingManifestParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) {
|
||||||
|
String value = parser.getAttributeValue(null, key);
|
||||||
|
if (value != null) {
|
||||||
|
return Boolean.parseBoolean(value);
|
||||||
|
} else {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SmoothStreamMediaParser extends ElementParser {
|
private static class SmoothStreamMediaParser extends ElementParser {
|
||||||
@ -317,19 +323,23 @@ public class SmoothStreamingManifestParser {
|
|||||||
private static final String KEY_MAJOR_VERSION = "MajorVersion";
|
private static final String KEY_MAJOR_VERSION = "MajorVersion";
|
||||||
private static final String KEY_MINOR_VERSION = "MinorVersion";
|
private static final String KEY_MINOR_VERSION = "MinorVersion";
|
||||||
private static final String KEY_TIME_SCALE = "TimeScale";
|
private static final String KEY_TIME_SCALE = "TimeScale";
|
||||||
|
private static final String KEY_DVR_WINDOW_LENGTH = "DVRWindowLength";
|
||||||
private static final String KEY_DURATION = "Duration";
|
private static final String KEY_DURATION = "Duration";
|
||||||
private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
|
private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
|
||||||
|
private static final String KEY_IS_LIVE = "IsLive";
|
||||||
|
|
||||||
private int majorVersion;
|
private int majorVersion;
|
||||||
private int minorVersion;
|
private int minorVersion;
|
||||||
private long timeScale;
|
private long timescale;
|
||||||
private long duration;
|
private long duration;
|
||||||
|
private long dvrWindowLength;
|
||||||
private int lookAheadCount;
|
private int lookAheadCount;
|
||||||
|
private boolean isLive;
|
||||||
private ProtectionElement protectionElement;
|
private ProtectionElement protectionElement;
|
||||||
private List<StreamElement> streamElements;
|
private List<StreamElement> streamElements;
|
||||||
|
|
||||||
public SmoothStreamMediaParser(ElementParser parent) {
|
public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) {
|
||||||
super(TAG, parent);
|
super(parent, baseUri, TAG);
|
||||||
lookAheadCount = -1;
|
lookAheadCount = -1;
|
||||||
protectionElement = null;
|
protectionElement = null;
|
||||||
streamElements = new LinkedList<StreamElement>();
|
streamElements = new LinkedList<StreamElement>();
|
||||||
@ -339,10 +349,12 @@ public class SmoothStreamingManifestParser {
|
|||||||
public void parseStartTag(XmlPullParser parser) throws ParserException {
|
public void parseStartTag(XmlPullParser parser) throws ParserException {
|
||||||
majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);
|
majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);
|
||||||
minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);
|
minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);
|
||||||
timeScale = parseLong(parser, KEY_TIME_SCALE, 10000000L);
|
timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L);
|
||||||
duration = parseRequiredLong(parser, KEY_DURATION);
|
duration = parseRequiredLong(parser, KEY_DURATION);
|
||||||
|
dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0);
|
||||||
lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1);
|
lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1);
|
||||||
putNormalizedAttribute(KEY_TIME_SCALE, timeScale);
|
isLive = parseBoolean(parser, KEY_IS_LIVE, false);
|
||||||
|
putNormalizedAttribute(KEY_TIME_SCALE, timescale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -359,8 +371,8 @@ public class SmoothStreamingManifestParser {
|
|||||||
public Object build() {
|
public Object build() {
|
||||||
StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
|
StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
|
||||||
streamElements.toArray(streamElementArray);
|
streamElements.toArray(streamElementArray);
|
||||||
return new SmoothStreamingManifest(majorVersion, minorVersion, timeScale, duration,
|
return new SmoothStreamingManifest(majorVersion, minorVersion, timescale, duration,
|
||||||
lookAheadCount, protectionElement, streamElementArray);
|
dvrWindowLength, lookAheadCount, isLive, protectionElement, streamElementArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -376,8 +388,8 @@ public class SmoothStreamingManifestParser {
|
|||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
private byte[] initData;
|
private byte[] initData;
|
||||||
|
|
||||||
public ProtectionElementParser(ElementParser parent) {
|
public ProtectionElementParser(ElementParser parent, Uri baseUri) {
|
||||||
super(TAG, parent);
|
super(parent, baseUri, TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -426,7 +438,6 @@ public class SmoothStreamingManifestParser {
|
|||||||
private static final String KEY_TYPE_TEXT = "text";
|
private static final String KEY_TYPE_TEXT = "text";
|
||||||
private static final String KEY_SUB_TYPE = "Subtype";
|
private static final String KEY_SUB_TYPE = "Subtype";
|
||||||
private static final String KEY_NAME = "Name";
|
private static final String KEY_NAME = "Name";
|
||||||
private static final String KEY_CHUNKS = "Chunks";
|
|
||||||
private static final String KEY_QUALITY_LEVELS = "QualityLevels";
|
private static final String KEY_QUALITY_LEVELS = "QualityLevels";
|
||||||
private static final String KEY_URL = "Url";
|
private static final String KEY_URL = "Url";
|
||||||
private static final String KEY_MAX_WIDTH = "MaxWidth";
|
private static final String KEY_MAX_WIDTH = "MaxWidth";
|
||||||
@ -439,11 +450,12 @@ public class SmoothStreamingManifestParser {
|
|||||||
private static final String KEY_FRAGMENT_DURATION = "d";
|
private static final String KEY_FRAGMENT_DURATION = "d";
|
||||||
private static final String KEY_FRAGMENT_START_TIME = "t";
|
private static final String KEY_FRAGMENT_START_TIME = "t";
|
||||||
|
|
||||||
|
private final Uri baseUri;
|
||||||
private final List<TrackElement> tracks;
|
private final List<TrackElement> tracks;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String subType;
|
private String subType;
|
||||||
private long timeScale;
|
private long timescale;
|
||||||
private String name;
|
private String name;
|
||||||
private int qualityLevels;
|
private int qualityLevels;
|
||||||
private String url;
|
private String url;
|
||||||
@ -452,13 +464,13 @@ public class SmoothStreamingManifestParser {
|
|||||||
private int displayWidth;
|
private int displayWidth;
|
||||||
private int displayHeight;
|
private int displayHeight;
|
||||||
private String language;
|
private String language;
|
||||||
private long[] startTimes;
|
private ArrayList<Long> startTimes;
|
||||||
|
|
||||||
private int chunkIndex;
|
|
||||||
private long previousChunkDuration;
|
private long previousChunkDuration;
|
||||||
|
|
||||||
public StreamElementParser(ElementParser parent) {
|
public StreamElementParser(ElementParser parent, Uri baseUri) {
|
||||||
super(TAG, parent);
|
super(parent, baseUri, TAG);
|
||||||
|
this.baseUri = baseUri;
|
||||||
tracks = new LinkedList<TrackElement>();
|
tracks = new LinkedList<TrackElement>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,19 +489,21 @@ public class SmoothStreamingManifestParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
|
private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
|
||||||
startTimes[chunkIndex] = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
|
int chunkIndex = startTimes.size();
|
||||||
if (startTimes[chunkIndex] == -1L) {
|
long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
|
||||||
|
if (startTime == -1L) {
|
||||||
if (chunkIndex == 0) {
|
if (chunkIndex == 0) {
|
||||||
// Assume the track starts at t = 0.
|
// Assume the track starts at t = 0.
|
||||||
startTimes[chunkIndex] = 0;
|
startTime = 0;
|
||||||
} else if (previousChunkDuration != -1L) {
|
} else if (previousChunkDuration != -1L) {
|
||||||
// Infer the start time from the previous chunk's start time and duration.
|
// Infer the start time from the previous chunk's start time and duration.
|
||||||
startTimes[chunkIndex] = startTimes[chunkIndex - 1] + previousChunkDuration;
|
startTime = startTimes.get(chunkIndex - 1) + previousChunkDuration;
|
||||||
} else {
|
} else {
|
||||||
// We don't have the start time, and we're unable to infer it.
|
// We don't have the start time, and we're unable to infer it.
|
||||||
throw new ParserException("Unable to infer start time");
|
throw new ParserException("Unable to infer start time");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
startTimes.add(startTime);
|
||||||
previousChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
|
previousChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
|
||||||
chunkIndex++;
|
chunkIndex++;
|
||||||
}
|
}
|
||||||
@ -510,11 +524,11 @@ public class SmoothStreamingManifestParser {
|
|||||||
displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1);
|
displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1);
|
||||||
displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1);
|
displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1);
|
||||||
language = parser.getAttributeValue(null, KEY_LANGUAGE);
|
language = parser.getAttributeValue(null, KEY_LANGUAGE);
|
||||||
timeScale = parseInt(parser, KEY_TIME_SCALE, -1);
|
timescale = parseInt(parser, KEY_TIME_SCALE, -1);
|
||||||
if (timeScale == -1) {
|
if (timescale == -1) {
|
||||||
timeScale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
|
timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
|
||||||
}
|
}
|
||||||
startTimes = new long[parseRequiredInt(parser, KEY_CHUNKS)];
|
startTimes = new ArrayList<Long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int parseType(XmlPullParser parser) throws ParserException {
|
private int parseType(XmlPullParser parser) throws ParserException {
|
||||||
@ -544,8 +558,8 @@ public class SmoothStreamingManifestParser {
|
|||||||
public Object build() {
|
public Object build() {
|
||||||
TrackElement[] trackElements = new TrackElement[tracks.size()];
|
TrackElement[] trackElements = new TrackElement[tracks.size()];
|
||||||
tracks.toArray(trackElements);
|
tracks.toArray(trackElements);
|
||||||
return new StreamElement(type, subType, timeScale, name, qualityLevels, url, maxWidth,
|
return new StreamElement(baseUri, url, type, subType, timescale, name, qualityLevels,
|
||||||
maxHeight, displayWidth, displayHeight, language, trackElements, startTimes);
|
maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -586,8 +600,8 @@ public class SmoothStreamingManifestParser {
|
|||||||
private int nalUnitLengthField;
|
private int nalUnitLengthField;
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
public TrackElementParser(ElementParser parent) {
|
public TrackElementParser(ElementParser parent, Uri baseUri) {
|
||||||
super(TAG, parent);
|
super(parent, baseUri, TAG);
|
||||||
this.csd = new LinkedList<byte[]>();
|
this.csd = new LinkedList<byte[]>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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.smoothstreaming;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||||
|
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
|
||||||
|
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SmoothStreamingUtil {
|
||||||
|
|
||||||
|
private SmoothStreamingUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link MediaFormat} for the specified track of the specified {@link StreamElement}.
|
||||||
|
*
|
||||||
|
* @param element The stream element.
|
||||||
|
* @param track The index of the track for which to build the format.
|
||||||
|
* @return The format.
|
||||||
|
*/
|
||||||
|
public static MediaFormat getMediaFormat(StreamElement element, int track) {
|
||||||
|
TrackElement trackElement = element.tracks[track];
|
||||||
|
String mimeType = trackElement.mimeType;
|
||||||
|
if (element.type == StreamElement.TYPE_VIDEO) {
|
||||||
|
MediaFormat format = MediaFormat.createVideoFormat(mimeType, -1, trackElement.maxWidth,
|
||||||
|
trackElement.maxHeight, Arrays.asList(trackElement.csd));
|
||||||
|
format.setMaxVideoDimensions(element.maxWidth, element.maxHeight);
|
||||||
|
return format;
|
||||||
|
} else if (element.type == StreamElement.TYPE_AUDIO) {
|
||||||
|
List<byte[]> csd;
|
||||||
|
if (trackElement.csd != null) {
|
||||||
|
csd = Arrays.asList(trackElement.csd);
|
||||||
|
} else {
|
||||||
|
csd = Collections.singletonList(CodecSpecificDataUtil.buildAudioSpecificConfig(
|
||||||
|
trackElement.sampleRate, trackElement.numChannels));
|
||||||
|
}
|
||||||
|
MediaFormat format = MediaFormat.createAudioFormat(mimeType, -1, trackElement.numChannels,
|
||||||
|
trackElement.sampleRate, csd);
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
// TODO: Do subtitles need a format? MediaFormat supports KEY_LANGUAGE.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getKeyId(byte[] initData) {
|
||||||
|
StringBuilder initDataStringBuilder = new StringBuilder();
|
||||||
|
for (int i = 0; i < initData.length; i += 2) {
|
||||||
|
initDataStringBuilder.append((char) initData[i]);
|
||||||
|
}
|
||||||
|
String initDataString = initDataStringBuilder.toString();
|
||||||
|
String keyIdString = initDataString.substring(
|
||||||
|
initDataString.indexOf("<KID>") + 5, initDataString.indexOf("</KID>"));
|
||||||
|
byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT);
|
||||||
|
swap(keyId, 0, 3);
|
||||||
|
swap(keyId, 1, 2);
|
||||||
|
swap(keyId, 4, 5);
|
||||||
|
swap(keyId, 6, 7);
|
||||||
|
return keyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void swap(byte[] data, int firstPosition, int secondPosition) {
|
||||||
|
byte temp = data[firstPosition];
|
||||||
|
data[firstPosition] = data[secondPosition];
|
||||||
|
data[secondPosition] = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.text;
|
package com.google.android.exoplayer.text;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.ExoPlaybackException;
|
import com.google.android.exoplayer.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer.MediaFormatHolder;
|
import com.google.android.exoplayer.MediaFormatHolder;
|
||||||
import com.google.android.exoplayer.SampleHolder;
|
import com.google.android.exoplayer.SampleHolder;
|
||||||
@ -177,7 +178,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
|
|||||||
resetSampleHolder = true;
|
resetSampleHolder = true;
|
||||||
InputStream subtitleInputStream =
|
InputStream subtitleInputStream =
|
||||||
new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size);
|
new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size);
|
||||||
subtitle = subtitleParser.parse(subtitleInputStream, "UTF-8", sampleHolder.timeUs);
|
subtitle = subtitleParser.parse(subtitleInputStream, C.UTF8_NAME, sampleHolder.timeUs);
|
||||||
syncNextEventIndex(timeUs);
|
syncNextEventIndex(timeUs);
|
||||||
textRendererNeedsUpdate = true;
|
textRendererNeedsUpdate = true;
|
||||||
} else if (result == SampleSource.END_OF_STREAM) {
|
} else if (result == SampleSource.END_OF_STREAM) {
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.util;
|
package com.google.android.exoplayer.util;
|
||||||
|
|
||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.C;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
@ -30,7 +30,7 @@ import java.net.URL;
|
|||||||
*
|
*
|
||||||
* @param <T> The type of the manifest being parsed.
|
* @param <T> The type of the manifest being parsed.
|
||||||
*/
|
*/
|
||||||
public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
public class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked with the result of a manifest fetch.
|
* Invoked with the result of a manifest fetch.
|
||||||
@ -59,6 +59,7 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
|||||||
|
|
||||||
public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000;
|
public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000;
|
||||||
|
|
||||||
|
private final ManifestParser<T> parser;
|
||||||
private final ManifestCallback<T> callback;
|
private final ManifestCallback<T> callback;
|
||||||
private final int timeoutMillis;
|
private final int timeoutMillis;
|
||||||
|
|
||||||
@ -68,15 +69,18 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
|||||||
/**
|
/**
|
||||||
* @param callback The callback to provide with the parsed manifest (or error).
|
* @param callback The callback to provide with the parsed manifest (or error).
|
||||||
*/
|
*/
|
||||||
public ManifestFetcher(ManifestCallback<T> callback) {
|
public ManifestFetcher(ManifestParser<T> parser, ManifestCallback<T> callback) {
|
||||||
this(callback, DEFAULT_HTTP_TIMEOUT_MILLIS);
|
this(parser, callback, DEFAULT_HTTP_TIMEOUT_MILLIS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param parser Parses the manifest from the loaded data.
|
||||||
* @param callback The callback to provide with the parsed manifest (or error).
|
* @param callback The callback to provide with the parsed manifest (or error).
|
||||||
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
|
* @param timeoutMillis The timeout in milliseconds for the connection used to load the data.
|
||||||
*/
|
*/
|
||||||
public ManifestFetcher(ManifestCallback<T> callback, int timeoutMillis) {
|
public ManifestFetcher(ManifestParser<T> parser, ManifestCallback<T> callback,
|
||||||
|
int timeoutMillis) {
|
||||||
|
this.parser = parser;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.timeoutMillis = timeoutMillis;
|
this.timeoutMillis = timeoutMillis;
|
||||||
}
|
}
|
||||||
@ -89,11 +93,14 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
|||||||
String inputEncoding = null;
|
String inputEncoding = null;
|
||||||
InputStream inputStream = null;
|
InputStream inputStream = null;
|
||||||
try {
|
try {
|
||||||
Uri baseUrl = Util.parseBaseUri(urlString);
|
Uri baseUri = Util.parseBaseUri(urlString);
|
||||||
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
|
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
|
||||||
inputStream = connection.getInputStream();
|
inputStream = connection.getInputStream();
|
||||||
inputEncoding = connection.getContentEncoding();
|
inputEncoding = connection.getContentEncoding();
|
||||||
return parse(inputStream, inputEncoding, contentId, baseUrl);
|
if (inputEncoding == null) {
|
||||||
|
inputEncoding = C.UTF8_NAME;
|
||||||
|
}
|
||||||
|
return parser.parse(inputStream, inputEncoding, contentId, baseUri);
|
||||||
} finally {
|
} finally {
|
||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
inputStream.close();
|
inputStream.close();
|
||||||
@ -114,21 +121,6 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the {@link InputStream} and parses it into a manifest. Invoked from the
|
|
||||||
* {@link AsyncTask}'s background thread.
|
|
||||||
*
|
|
||||||
* @param stream The input stream to read.
|
|
||||||
* @param inputEncoding The encoding of the input stream.
|
|
||||||
* @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 ParserException If an error occurred parsing the loaded data.
|
|
||||||
*/
|
|
||||||
protected abstract T parse(InputStream stream, String inputEncoding, String contentId,
|
|
||||||
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();
|
||||||
connection.setConnectTimeout(timeoutMillis);
|
connection.setConnectTimeout(timeoutMillis);
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer.util;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.ParserException;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a manifest from an {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @param <T> The type of the manifest being parsed.
|
||||||
|
*/
|
||||||
|
public interface ManifestParser<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a manifest from an {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to consume.
|
||||||
|
* @param inputEncoding The encoding of the input stream.
|
||||||
|
* @param contentId The content id to which the manifest corresponds. May be null.
|
||||||
|
* @param baseUri If the manifest contains relative uris, this is the uri they are relative to.
|
||||||
|
* May be null.
|
||||||
|
* @return The parsed manifest.
|
||||||
|
* @throws IOException If an error occurs reading the data.
|
||||||
|
* @throws ParserException If an error occurs parsing the data.
|
||||||
|
*/
|
||||||
|
T parse(InputStream inputStream, String inputEncoding, String contentId, Uri baseUri)
|
||||||
|
throws IOException, ParserException;
|
||||||
|
|
||||||
|
}
|
@ -18,17 +18,25 @@ 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 android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Miscellaneous utility functions.
|
* Miscellaneous utility functions.
|
||||||
@ -41,6 +49,14 @@ public final class Util {
|
|||||||
*/
|
*/
|
||||||
public static final int SDK_INT = android.os.Build.VERSION.SDK_INT;
|
public static final int SDK_INT = android.os.Build.VERSION.SDK_INT;
|
||||||
|
|
||||||
|
private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile(
|
||||||
|
"(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
|
||||||
|
+ "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?"
|
||||||
|
+ "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
|
||||||
|
|
||||||
|
private static final Pattern XS_DURATION_PATTERN =
|
||||||
|
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
|
||||||
|
|
||||||
private Util() {}
|
private Util() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,6 +144,32 @@ public final class Util {
|
|||||||
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
|
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges a uri and a string to produce a new 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 uri consists solely of {@code stringUri}.
|
||||||
|
* <li>If {@code stringUri} is null, then the uri consists solely of {@code baseUrl}.
|
||||||
|
* <li>Otherwise, the uri consists of the concatenation of {@code baseUri} and {@code stringUri}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param baseUri A uri that can form the base of the merged uri.
|
||||||
|
* @param stringUri A relative or absolute uri in string form.
|
||||||
|
* @return The merged uri.
|
||||||
|
*/
|
||||||
|
public static Uri getMergedUri(Uri baseUri, String stringUri) {
|
||||||
|
if (stringUri == null) {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
Uri uri = Uri.parse(stringUri);
|
||||||
|
if (!uri.isAbsolute() && baseUri != null) {
|
||||||
|
uri = Uri.withAppendedPath(baseUri, stringUri);
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of the largest value in an array that is less than (or optionally equal to)
|
* Returns the index of the largest value in an array that is less than (or optionally equal to)
|
||||||
* a specified key.
|
* a specified key.
|
||||||
@ -212,4 +254,76 @@ public final class Util {
|
|||||||
return stayInBounds ? Math.min(list.size() - 1, index) : index;
|
return stayInBounds ? Math.min(list.size() - 1, index) : index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an xs:duration attribute value, returning the parsed duration in milliseconds.
|
||||||
|
*
|
||||||
|
* @param value The attribute value to parse.
|
||||||
|
* @return The parsed duration in milliseconds.
|
||||||
|
*/
|
||||||
|
public static long parseXsDuration(String value) {
|
||||||
|
Matcher matcher = XS_DURATION_PATTERN.matcher(value);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
String hours = matcher.group(2);
|
||||||
|
double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
|
||||||
|
String minutes = matcher.group(4);
|
||||||
|
durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
|
||||||
|
String seconds = matcher.group(6);
|
||||||
|
durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
|
||||||
|
return (long) (durationSeconds * 1000);
|
||||||
|
} else {
|
||||||
|
return (long) (Double.parseDouble(value) * 3600 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since
|
||||||
|
* the epoch.
|
||||||
|
*
|
||||||
|
* @param value The attribute value to parse.
|
||||||
|
* @return The parsed timestamp in milliseconds since the epoch.
|
||||||
|
*/
|
||||||
|
public static long parseXsDateTime(String value) throws ParseException {
|
||||||
|
Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
throw new ParseException("Invalid date/time format: " + value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int timezoneShift;
|
||||||
|
if (matcher.group(9) == null) {
|
||||||
|
// No time zone specified.
|
||||||
|
timezoneShift = 0;
|
||||||
|
} else if (matcher.group(9).equalsIgnoreCase("Z")) {
|
||||||
|
timezoneShift = 0;
|
||||||
|
} else {
|
||||||
|
timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60
|
||||||
|
+ Integer.valueOf(matcher.group(13))));
|
||||||
|
if (matcher.group(11).equals("-")) {
|
||||||
|
timezoneShift *= -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||||
|
|
||||||
|
dateTime.clear();
|
||||||
|
// Note: The month value is 0-based, hence the -1 on group(2)
|
||||||
|
dateTime.set(Integer.valueOf(matcher.group(1)),
|
||||||
|
Integer.valueOf(matcher.group(2)) - 1,
|
||||||
|
Integer.valueOf(matcher.group(3)),
|
||||||
|
Integer.valueOf(matcher.group(4)),
|
||||||
|
Integer.valueOf(matcher.group(5)),
|
||||||
|
Integer.valueOf(matcher.group(6)));
|
||||||
|
if (!TextUtils.isEmpty(matcher.group(8))) {
|
||||||
|
final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
|
||||||
|
// we care only for milliseconds, so movePointRight(3)
|
||||||
|
dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
long time = dateTime.getTimeInMillis();
|
||||||
|
if (timezoneShift != 0) {
|
||||||
|
time -= timezoneShift * 60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user