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:
Oliver Woodman 2014-09-11 16:19:26 +01:00
parent d4e35358a1
commit bf5ee6ff23
18 changed files with 444 additions and 372 deletions

View File

@ -31,7 +31,7 @@ import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.Representation;
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.DefaultBandwidthMeter;
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.MimeTypes;
import com.google.android.exoplayer.util.Util;
@ -94,7 +95,9 @@ public class DashVodRendererBuilder implements RendererBuilder,
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
this.player = player;
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);
}

View File

@ -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.StreamElement;
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.ttml.TtmlParser;
import com.google.android.exoplayer.upstream.BufferPool;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
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.Util;
@ -87,8 +88,11 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
this.player = player;
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
@ -151,7 +155,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
@ -178,7 +182,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
for (int i = 0; i < manifest.streamElements.length; i++) {
if (manifest.streamElements[i].type == StreamElement.TYPE_AUDIO) {
audioTrackNames[audioStreamElementCount] = manifest.streamElements[i].name;
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
audioChunkSources[audioStreamElementCount] = new SmoothStreamingChunkSource(manifest,
i, new int[] {0}, audioDataSource, audioFormatEvaluator);
audioStreamElementCount++;
}
@ -208,7 +212,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
for (int i = 0; i < manifest.streamElements.length; i++) {
if (manifest.streamElements[i].type == StreamElement.TYPE_TEXT) {
textTrackNames[textStreamElementCount] = manifest.streamElements[i].language;
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(url, manifest,
textChunkSources[textStreamElementCount] = new SmoothStreamingChunkSource(manifest,
i, new int[] {0}, ttmlDataSource, ttmlFormatEvaluator);
textStreamElementCount++;
}

View File

@ -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.mpd.AdaptationSet;
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.Representation;
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.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.media.MediaCodec;
@ -74,7 +75,9 @@ import java.util.ArrayList;
@Override
public void buildRenderers(RendererBuilderCallback 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);
}

View File

@ -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.StreamElement;
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.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
import android.media.MediaCodec;
@ -71,8 +72,10 @@ import java.util.ArrayList;
@Override
public void buildRenderers(RendererBuilderCallback 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
@ -116,9 +119,8 @@ import java.util.ArrayList;
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifest, videoStreamElementIndex,
videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter));
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource,
@ -126,9 +128,8 @@ import java.util.ArrayList;
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
audioStreamElementIndex, new int[] {0}, audioDataSource,
new FormatEvaluator.FixedEvaluator());
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifest, audioStreamElementIndex,
new int[] {0}, audioDataSource, new FormatEvaluator.FixedEvaluator());
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(

View File

@ -25,6 +25,11 @@ public final class C {
*/
public static final int LENGTH_UNBOUNDED = -1;
/**
* The name of the UTF-8 charset.
*/
public static final String UTF8_NAME = "UTF-8";
private C() {}
}

View File

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

View File

@ -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.SingleSegmentBase;
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.Util;
import android.net.Uri;
import android.text.TextUtils;
@ -34,29 +36,15 @@ import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A parser of media presentation description files.
*/
public class MediaPresentationDescriptionParser extends DefaultHandler {
// 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)))?");
public class MediaPresentationDescriptionParser extends DefaultHandler
implements ManifestParser<MediaPresentationDescription> {
private final XmlPullParserFactory xmlParserFactory;
@ -70,19 +58,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
// MPD parsing.
/**
* Parses a manifest from the provided {@link InputStream}.
*
* @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 {
@Override
public MediaPresentationDescription parse(InputStream inputStream, String inputEncoding,
String contentId, Uri baseUrl) throws IOException, ParserException {
try {
XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding);
@ -102,12 +80,13 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1);
long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1);
String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1;
long minUpdateTimeMs = (dynamic) ? parseDuration(xpp, "minimumUpdatePeriod", -1) : -1;
long timeShiftBufferDepthMs = (dynamic) ? parseDuration(xpp, "timeShiftBufferDepth", -1)
: -1;
UtcTimingElement utcTiming = null;
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)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
long startMs = parseDurationMs(xpp, "start", 0);
long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs);
long startMs = parseDuration(xpp, "start", 0);
long durationMs = parseDuration(xpp, "duration", mpdDurationMs);
SegmentBase segmentBase = null;
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
do {
@ -450,85 +429,25 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
}
private static long parseDurationMs(XmlPullParser xpp, String name) {
return parseDurationMs(xpp, name, -1);
private static long parseDuration(XmlPullParser xpp, String name, long defaultValue) {
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)
throws ParseException {
String value = xpp.getAttributeValue(null, name);
if (value == null) {
return defaultValue;
} 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)
throws XmlPullParserException, IOException {
xpp.next();

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util;
import android.net.Uri;
@ -47,15 +48,10 @@ public final class RangedUri {
/**
* 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>
* See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and
* {@code stringUri} are merged.
*
* @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 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.
@ -74,14 +70,7 @@ public final class RangedUri {
* @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;
return Util.getMergedUri(baseUri, stringUri);
}
/**

View File

@ -15,6 +15,7 @@
*/
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.util.Assertions;
@ -210,7 +211,7 @@ import java.util.Stack;
if (stringResult != READ_RESULT_CONTINUE) {
return stringResult;
}
String stringValue = new String(stringBytes, Charset.forName("UTF-8"));
String stringValue = new String(stringBytes, Charset.forName(C.UTF8_NAME));
stringBytes = null;
eventHandler.onStringElement(elementId, stringValue);
prepareForNextElement();

View File

@ -53,7 +53,6 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private static final int INITIALIZATION_VECTOR_SIZE = 8;
private final String baseUrl;
private final StreamElement streamElement;
private final TrackInfo trackInfo;
private final DataSource dataSource;
@ -67,7 +66,6 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private final SmoothStreamingFormat[] formats;
/**
* @param baseUrl The base URL for the streams.
* @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}.
* @param streamElementIndex The index of the stream element in the manifest to be provided by
* the source.
@ -76,10 +74,8 @@ public class SmoothStreamingChunkSource implements ChunkSource {
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param formatEvaluator Selects from the available formats.
*/
public SmoothStreamingChunkSource(String baseUrl, SmoothStreamingManifest manifest,
int streamElementIndex, int[] trackIndices, DataSource dataSource,
FormatEvaluator formatEvaluator) {
this.baseUrl = baseUrl;
public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, int streamElementIndex,
int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
this.streamElement = manifest.streamElements[streamElementIndex];
this.trackInfo = new TrackInfo(streamElement.tracks[0].mimeType, manifest.getDurationUs());
this.dataSource = dataSource;
@ -113,7 +109,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
: Track.TYPE_AUDIO;
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));
if (protectionElement != null) {
extractor.putPsshInfo(protectionElement.uuid, protectionElement.data);
@ -183,9 +179,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
boolean isLastChunk = nextChunkIndex == streamElement.chunkCount - 1;
String requestUrl = streamElement.buildRequestUrl(selectedFormat.trackIndex,
nextChunkIndex);
Uri uri = Uri.parse(baseUrl + '/' + requestUrl);
Uri uri = streamElement.buildRequestUri(selectedFormat.trackIndex, nextChunkIndex);
Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null,
extractors.get(Integer.parseInt(selectedFormat.id)), dataSource, nextChunkIndex,
isLastChunk, streamElement.getStartTimeUs(nextChunkIndex),

View File

@ -15,9 +15,13 @@
*/
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.Util;
import android.net.Uri;
import java.util.List;
import java.util.UUID;
/**
@ -30,32 +34,48 @@ public class SmoothStreamingManifest {
public final int majorVersion;
public final int minorVersion;
public final long timeScale;
public final long timescale;
public final int lookAheadCount;
public final boolean isLive;
public final ProtectionElement protectionElement;
public final StreamElement[] streamElements;
private final long duration;
private final long dvrWindowLength;
public SmoothStreamingManifest(int majorVersion, int minorVersion, long timeScale, long duration,
int lookAheadCount, ProtectionElement protectionElement, StreamElement[] streamElements) {
public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration,
long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
StreamElement[] streamElements) {
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
this.timeScale = timeScale;
this.timescale = timescale;
this.duration = duration;
this.dvrWindowLength = dvrWindowLength;
this.lookAheadCount = lookAheadCount;
this.isLive = isLive;
this.protectionElement = protectionElement;
this.streamElements = streamElements;
}
/**
* 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() {
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 String subType;
public final long timeScale;
public final long timescale;
public final String name;
public final int qualityLevels;
public final String url;
public final int maxWidth;
public final int maxHeight;
public final int displayWidth;
@ -167,24 +186,29 @@ public class SmoothStreamingManifest {
public final TrackElement[] tracks;
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,
int qualityLevels, String url, int maxWidth, int maxHeight, int displayWidth,
int displayHeight, String language, TrackElement[] tracks, long[] chunkStartTimes) {
private final List<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.subType = subType;
this.timeScale = timeScale;
this.timescale = timescale;
this.name = name;
this.qualityLevels = qualityLevels;
this.url = url;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.displayWidth = displayWidth;
this.displayHeight = displayHeight;
this.language = language;
this.tracks = tracks;
this.chunkCount = chunkStartTimes.length;
this.chunkCount = chunkStartTimes.size();
this.chunkStartTimes = chunkStartTimes;
}
@ -195,7 +219,7 @@ public class SmoothStreamingManifest {
* @return The index of the corresponding chunk.
*/
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.
*/
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 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) {
assert (tracks != null);
assert (chunkStartTimes != null);
assert (chunkIndex < chunkStartTimes.length);
return url.replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate))
.replace(URL_PLACEHOLDER_START_TIME, Long.toString(chunkStartTimes[chunkIndex]));
public Uri buildRequestUri(int track, int chunkIndex) {
Assertions.checkState(tracks != null);
Assertions.checkState(chunkStartTimes != null);
Assertions.checkState(chunkIndex < chunkStartTimes.size());
String chunkUrl = chunkTemplate
.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();
}
}

View File

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

View File

@ -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.util.Assertions;
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.Pair;
@ -31,6 +33,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
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">
* IIS Smooth Streaming Client Manifest Format</a>
*/
public class SmoothStreamingManifestParser {
public class SmoothStreamingManifestParser implements ManifestParser<SmoothStreamingManifest> {
private final XmlPullParserFactory xmlParserFactory;
@ -53,21 +56,13 @@ public class SmoothStreamingManifestParser {
}
}
/**
* Parses a manifest from the provided {@link InputStream}.
*
* @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 {
@Override
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding,
String contentId, Uri baseUri) throws IOException, ParserException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, inputEncoding);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, baseUri);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) {
throw new ParserException(e);
@ -90,14 +85,16 @@ public class SmoothStreamingManifestParser {
*/
private static abstract class ElementParser {
private final Uri baseUri;
private final String tag;
private final ElementParser parent;
private final List<Pair<String, Object>> normalizedAttributes;
public ElementParser(String tag, ElementParser parent) {
this.tag = tag;
public ElementParser(ElementParser parent, Uri baseUri, String tag) {
this.parent = parent;
this.baseUri = baseUri;
this.tag = tag;
this.normalizedAttributes = new LinkedList<Pair<String, Object>>();
}
@ -120,7 +117,7 @@ public class SmoothStreamingManifestParser {
} else if (handleChildInline(tagName)) {
parseStartTag(xmlParser);
} else {
ElementParser childElementParser = newChildParser(this, tagName);
ElementParser childElementParser = newChildParser(this, tagName, baseUri);
if (childElementParser == null) {
skippingElementDepth = 1;
} 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)) {
return new TrackElementParser(parent);
return new TrackElementParser(parent, baseUri);
} else if (ProtectionElementParser.TAG.equals(name)) {
return new ProtectionElementParser(parent);
return new ProtectionElementParser(parent, baseUri);
} else if (StreamElementParser.TAG.equals(name)) {
return new StreamElementParser(parent);
return new StreamElementParser(parent, baseUri);
}
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 {
@ -317,19 +323,23 @@ public class SmoothStreamingManifestParser {
private static final String KEY_MAJOR_VERSION = "MajorVersion";
private static final String KEY_MINOR_VERSION = "MinorVersion";
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_LOOKAHEAD_COUNT = "LookaheadCount";
private static final String KEY_IS_LIVE = "IsLive";
private int majorVersion;
private int minorVersion;
private long timeScale;
private long timescale;
private long duration;
private long dvrWindowLength;
private int lookAheadCount;
private boolean isLive;
private ProtectionElement protectionElement;
private List<StreamElement> streamElements;
public SmoothStreamMediaParser(ElementParser parent) {
super(TAG, parent);
public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) {
super(parent, baseUri, TAG);
lookAheadCount = -1;
protectionElement = null;
streamElements = new LinkedList<StreamElement>();
@ -339,10 +349,12 @@ public class SmoothStreamingManifestParser {
public void parseStartTag(XmlPullParser parser) throws ParserException {
majorVersion = parseRequiredInt(parser, KEY_MAJOR_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);
dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0);
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
@ -359,8 +371,8 @@ public class SmoothStreamingManifestParser {
public Object build() {
StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
streamElements.toArray(streamElementArray);
return new SmoothStreamingManifest(majorVersion, minorVersion, timeScale, duration,
lookAheadCount, protectionElement, streamElementArray);
return new SmoothStreamingManifest(majorVersion, minorVersion, timescale, duration,
dvrWindowLength, lookAheadCount, isLive, protectionElement, streamElementArray);
}
}
@ -376,8 +388,8 @@ public class SmoothStreamingManifestParser {
private UUID uuid;
private byte[] initData;
public ProtectionElementParser(ElementParser parent) {
super(TAG, parent);
public ProtectionElementParser(ElementParser parent, Uri baseUri) {
super(parent, baseUri, TAG);
}
@Override
@ -426,7 +438,6 @@ public class SmoothStreamingManifestParser {
private static final String KEY_TYPE_TEXT = "text";
private static final String KEY_SUB_TYPE = "Subtype";
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_URL = "Url";
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_START_TIME = "t";
private final Uri baseUri;
private final List<TrackElement> tracks;
private int type;
private String subType;
private long timeScale;
private long timescale;
private String name;
private int qualityLevels;
private String url;
@ -452,13 +464,13 @@ public class SmoothStreamingManifestParser {
private int displayWidth;
private int displayHeight;
private String language;
private long[] startTimes;
private ArrayList<Long> startTimes;
private int chunkIndex;
private long previousChunkDuration;
public StreamElementParser(ElementParser parent) {
super(TAG, parent);
public StreamElementParser(ElementParser parent, Uri baseUri) {
super(parent, baseUri, TAG);
this.baseUri = baseUri;
tracks = new LinkedList<TrackElement>();
}
@ -477,19 +489,21 @@ public class SmoothStreamingManifestParser {
}
private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
startTimes[chunkIndex] = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
if (startTimes[chunkIndex] == -1L) {
int chunkIndex = startTimes.size();
long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
if (startTime == -1L) {
if (chunkIndex == 0) {
// Assume the track starts at t = 0.
startTimes[chunkIndex] = 0;
startTime = 0;
} else if (previousChunkDuration != -1L) {
// 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 {
// We don't have the start time, and we're unable to infer it.
throw new ParserException("Unable to infer start time");
}
}
startTimes.add(startTime);
previousChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
chunkIndex++;
}
@ -510,11 +524,11 @@ public class SmoothStreamingManifestParser {
displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1);
displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1);
language = parser.getAttributeValue(null, KEY_LANGUAGE);
timeScale = parseInt(parser, KEY_TIME_SCALE, -1);
if (timeScale == -1) {
timeScale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
timescale = parseInt(parser, KEY_TIME_SCALE, -1);
if (timescale == -1) {
timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
}
startTimes = new long[parseRequiredInt(parser, KEY_CHUNKS)];
startTimes = new ArrayList<Long>();
}
private int parseType(XmlPullParser parser) throws ParserException {
@ -544,8 +558,8 @@ public class SmoothStreamingManifestParser {
public Object build() {
TrackElement[] trackElements = new TrackElement[tracks.size()];
tracks.toArray(trackElements);
return new StreamElement(type, subType, timeScale, name, qualityLevels, url, maxWidth,
maxHeight, displayWidth, displayHeight, language, trackElements, startTimes);
return new StreamElement(baseUri, url, type, subType, timescale, name, qualityLevels,
maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes);
}
}
@ -586,8 +600,8 @@ public class SmoothStreamingManifestParser {
private int nalUnitLengthField;
private String content;
public TrackElementParser(ElementParser parent) {
super(TAG, parent);
public TrackElementParser(ElementParser parent, Uri baseUri) {
super(parent, baseUri, TAG);
this.csd = new LinkedList<byte[]>();
}

View File

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

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer.text;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
@ -177,7 +178,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
resetSampleHolder = true;
InputStream subtitleInputStream =
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);
textRendererNeedsUpdate = true;
} else if (result == SampleSource.END_OF_STREAM) {

View File

@ -15,7 +15,7 @@
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.C;
import android.net.Uri;
import android.os.AsyncTask;
@ -30,7 +30,7 @@ import java.net.URL;
*
* @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.
@ -59,6 +59,7 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000;
private final ManifestParser<T> parser;
private final ManifestCallback<T> callback;
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).
*/
public ManifestFetcher(ManifestCallback<T> callback) {
this(callback, DEFAULT_HTTP_TIMEOUT_MILLIS);
public ManifestFetcher(ManifestParser<T> parser, ManifestCallback<T> callback) {
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 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.timeoutMillis = timeoutMillis;
}
@ -89,11 +93,14 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
String inputEncoding = null;
InputStream inputStream = null;
try {
Uri baseUrl = Util.parseBaseUri(urlString);
Uri baseUri = Util.parseBaseUri(urlString);
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding();
return parse(inputStream, inputEncoding, contentId, baseUrl);
if (inputEncoding == null) {
inputEncoding = C.UTF8_NAME;
}
return parser.parse(inputStream, inputEncoding, contentId, baseUri);
} finally {
if (inputStream != null) {
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 {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(timeoutMillis);

View File

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

View File

@ -18,17 +18,25 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.DataSource;
import android.net.Uri;
import android.text.TextUtils;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Miscellaneous utility functions.
@ -41,6 +49,14 @@ public final class Util {
*/
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() {}
/**
@ -128,6 +144,32 @@ public final class Util {
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)
* a specified key.
@ -212,4 +254,76 @@ public final class Util {
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;
}
}