diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 4ee13a75fc..966ce7a43b 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -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 mpdFetcher = + new ManifestFetcher(parser, this); mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index 5a4e9a58cb..af806e9fe7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -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 manifestFetcher = + new ManifestFetcher(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++; } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index a253de873e..cc994543b9 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -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 mpdFetcher = + new ManifestFetcher(parser, this); mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index 0b92810073..efde2de096 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -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 manifestFetcher = + new ManifestFetcher(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( diff --git a/library/src/main/java/com/google/android/exoplayer/C.java b/library/src/main/java/com/google/android/exoplayer/C.java index c8cd9fe586..f0f2a57fec 100644 --- a/library/src/main/java/com/google/android/exoplayer/C.java +++ b/library/src/main/java/com/google/android/exoplayer/C.java @@ -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() {} } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java deleted file mode 100644 index 45885cfc90..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java +++ /dev/null @@ -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. - *

- * 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 { - - private final MediaPresentationDescriptionParser parser; - - /** - * @param callback The callback to provide with the parsed manifest (or error). - */ - public MediaPresentationDescriptionFetcher( - ManifestCallback 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 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); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 0011c5d225..606f91a4ef 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -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 { 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 periods = new ArrayList(); @@ -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 adaptationSets = new ArrayList(); 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(); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java index cd18f85599..2ce5ad3092 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java @@ -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. *

- * The uri is built according to the following rules: - *

    - *
  • If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is - * ignored and the url consists solely of {@code stringUri}. - *
  • If {@code stringUri} is null, then the url consists solely of {@code baseUrl}. - *
  • Otherwise, the url consists of the concatenation of {@code baseUri} and {@code stringUri}. - *
+ * 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); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java index 55eca63de6..daea459e93 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/webm/DefaultEbmlReader.java @@ -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(); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 918ddb4f90..a6af466727 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -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), diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index d6a739ee1b..28f046816e 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -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. + *

+ * 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 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 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(); } } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java deleted file mode 100644 index 8fb6e66e40..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java +++ /dev/null @@ -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. - *

- * 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 { - - private final SmoothStreamingManifestParser parser; - - /** - * @param callback The callback to provide with the parsed manifest (or error). - */ - public SmoothStreamingManifestFetcher(ManifestCallback 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 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); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index b928cc5c16..46d0d41fed 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -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 * IIS Smooth Streaming Client Manifest Format */ -public class SmoothStreamingManifestParser { +public class SmoothStreamingManifestParser implements ManifestParser { 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> 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>(); } @@ -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 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(); @@ -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 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 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(); } @@ -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(); } 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(); } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java new file mode 100644 index 0000000000..7c5f8574f3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingUtil.java @@ -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 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("") + 5, initDataString.indexOf("")); + 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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index d6504461b2..db5440ead4 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -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) { diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index 5e362f4671..a8b10f2ba0 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -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 The type of the manifest being parsed. */ -public abstract class ManifestFetcher extends AsyncTask { +public class ManifestFetcher extends AsyncTask { /** * Invoked with the result of a manifest fetch. @@ -59,6 +59,7 @@ public abstract class ManifestFetcher extends AsyncTask { public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000; + private final ManifestParser parser; private final ManifestCallback callback; private final int timeoutMillis; @@ -68,15 +69,18 @@ public abstract class ManifestFetcher extends AsyncTask { /** * @param callback The callback to provide with the parsed manifest (or error). */ - public ManifestFetcher(ManifestCallback callback) { - this(callback, DEFAULT_HTTP_TIMEOUT_MILLIS); + public ManifestFetcher(ManifestParser parser, ManifestCallback 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 callback, int timeoutMillis) { + public ManifestFetcher(ManifestParser parser, ManifestCallback callback, + int timeoutMillis) { + this.parser = parser; this.callback = callback; this.timeoutMillis = timeoutMillis; } @@ -89,11 +93,14 @@ public abstract class ManifestFetcher extends AsyncTask { 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 extends AsyncTask { } } - /** - * 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); diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java new file mode 100644 index 0000000000..d3f1fab1e7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java @@ -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 The type of the manifest being parsed. + */ +public interface ManifestParser { + + /** + * 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; + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 5ebd400133..b4277f471a 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -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. + *

+ * The uri is built according to the following rules: + *

    + *
  • If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is + * ignored and the uri consists solely of {@code stringUri}. + *
  • If {@code stringUri} is null, then the uri consists solely of {@code baseUrl}. + *
  • Otherwise, the uri consists of the concatenation of {@code baseUri} and {@code stringUri}. + *
+ * + * @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; + } + }