Commit bf5ee6ff by Oliver Woodman

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.
parent d4e35358
Showing with 382 additions and 324 deletions
...@@ -31,7 +31,7 @@ import com.google.android.exoplayer.chunk.MultiTrackChunkSource; ...@@ -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; ...@@ -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, ...@@ -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; ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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, ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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 { ...@@ -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() {}
} }
/*
* 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; ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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,83 +429,23 @@ public class MediaPresentationDescriptionParser extends DefaultHandler { ...@@ -450,83 +429,23 @@ 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);
}
private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
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.parseXsDuration(value);
} }
} }
// VisibleForTesting private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
static long parseDateTime(String value) throws ParseException { 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); String value = xpp.getAttributeValue(null, name);
if (value != null) { if (value == null) {
Matcher matcher = DURATION.matcher(value); return defaultValue;
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 { } else {
return (long) (Double.parseDouble(value) * 3600 * 1000); return Util.parseXsDateTime(value);
}
} }
return defaultValue;
} }
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
......
...@@ -16,6 +16,7 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 @@ ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
private final List<Long> chunkStartTimes;
public StreamElement(int type, String subType, long timeScale, String name, public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType,
int qualityLevels, String url, int maxWidth, int maxHeight, int displayWidth, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight,
int displayHeight, String language, TrackElement[] tracks, long[] chunkStartTimes) { 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 { ...@@ -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 { ...@@ -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();
} }
} }
......
/*
* 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);
}
}
/*
* 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 @@ ...@@ -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 { ...@@ -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 @@ ...@@ -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; ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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);
......
/*
* 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; ...@@ -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 { ...@@ -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() {}
/** /**
...@@ -129,6 +145,32 @@ public final class Util { ...@@ -129,6 +145,32 @@ public final class Util {
} }
/** /**
* 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.
* <p> * <p>
...@@ -212,4 +254,76 @@ public final class Util { ...@@ -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;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment