/*
* 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.chunk.Format;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
import android.util.Log;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A parser of media presentation description files.
*/
/*
* TODO: Parse representation base attributes at multiple levels, and normalize the resulting
* datastructure.
* TODO: Decide how best to represent missing integer/double/long attributes.
*/
public class MediaPresentationDescriptionParser extends DefaultHandler {
private static final String TAG = "MediaPresentationDescriptionParser";
// 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 final XmlPullParserFactory xmlParserFactory;
public MediaPresentationDescriptionParser() {
try {
xmlParserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
}
}
/**
* 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.
* @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream.
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
*/
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
String inputEncoding, String contentId) throws XmlPullParserException, IOException,
ParserException {
XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding);
int eventType = xpp.next();
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
throw new ParserException(
"inputStream does not contain a valid media presentation description");
}
return parseMediaPresentationDescription(xpp, contentId);
}
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId) throws XmlPullParserException, IOException {
long duration = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTime = parseDurationMs(xpp, "minBufferTime");
String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
long minUpdateTime = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
List<Period> periods = new ArrayList<Period>();
do {
xpp.next();
if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, duration));
}
} while (!isEndTag(xpp, "MPD"));
return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime,
periods);
}
private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration)
throws XmlPullParserException, IOException {
int id = parseInt(xpp, "id");
long start = parseDurationMs(xpp, "start", 0);
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
List<Segment.Timeline> segmentTimelineList = null;
int segmentStartNumber = 0;
int segmentTimescale = 0;
long presentationTimeOffset = 0;
do {
xpp.next();
if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration,
segmentTimelineList));
} else if (isStartTag(xpp, "SegmentList")) {
segmentStartNumber = parseInt(xpp, "startNumber");
segmentTimescale = parseInt(xpp, "timescale");
presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
}
} while (!isEndTag(xpp, "Period"));
return new Period(id, start, duration, adaptationSets, segmentTimelineList,
segmentStartNumber, segmentTimescale, presentationTimeOffset);
}
private List<Segment.Timeline> parsePeriodSegmentList(
XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
List<Segment.Timeline> segmentTimelineList = new ArrayList<Segment.Timeline>();
do {
xpp.next();
if (isStartTag(xpp, "SegmentTimeline")) {
do {
xpp.next();
if (isStartTag(xpp, "S")) {
long duration = parseLong(xpp, "d");
segmentTimelineList.add(new Segment.Timeline(segmentStartNumber, duration));
segmentStartNumber++;
}
} while (!isEndTag(xpp, "SegmentTimeline"));
}
} while (!isEndTag(xpp, "SegmentList"));
return segmentTimelineList;
}
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart,
long periodDuration, List<Segment.Timeline> segmentTimelineList)
throws XmlPullParserException, IOException {
int id = -1;
int contentType = AdaptationSet.TYPE_UNKNOWN;
// TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9.
String mimeType = xpp.getAttributeValue(null, "mimeType");
if (mimeType != null) {
if (MimeTypes.isAudio(mimeType)) {
contentType = AdaptationSet.TYPE_AUDIO;
} else if (MimeTypes.isVideo(mimeType)) {
contentType = AdaptationSet.TYPE_VIDEO;
} else if (MimeTypes.isText(mimeType)
|| mimeType.equalsIgnoreCase(MimeTypes.APPLICATION_TTML)) {
contentType = AdaptationSet.TYPE_TEXT;
}
}
List<ContentProtection> contentProtections = null;
List<Representation> representations = new ArrayList<Representation>();
do {
xpp.next();
if (contentType != AdaptationSet.TYPE_UNKNOWN) {
if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>();
}
contentProtections.add(parseContentProtection(xpp));
} else if (isStartTag(xpp, "ContentComponent")) {
id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
String contentTypeString = xpp.getAttributeValue(null, "contentType");
contentType = "video".equals(contentTypeString) ? AdaptationSet.TYPE_VIDEO
: "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
: AdaptationSet.TYPE_UNKNOWN;
} else if (isStartTag(xpp, "Representation")) {
representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration,
mimeType, segmentTimelineList));
}
}
} while (!isEndTag(xpp, "AdaptationSet"));
return new AdaptationSet(id, contentType, representations, contentProtections);
}
/**
* Parses a ContentProtection element.
*
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
**/
protected ContentProtection parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeUriId = xpp.getAttributeValue(null, "schemeUriId");
return new ContentProtection(schemeUriId, null);
}
private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart,
long periodDuration, String parentMimeType, List<Segment.Timeline> segmentTimelineList)
throws XmlPullParserException, IOException {
int id;
try {
id = parseInt(xpp, "id");
} catch (NumberFormatException nfe) {
Log.d(TAG, "Unable to parse id; " + nfe.getMessage());
// TODO: need a way to generate a unique and stable id; use hashCode for now
id = xpp.getAttributeValue(null, "id").hashCode();
}
int bandwidth = parseInt(xpp, "bandwidth") / 8;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
int width = parseInt(xpp, "width");
int height = parseInt(xpp, "height");
String mimeType = xpp.getAttributeValue(null, "mimeType");
if (mimeType == null) {
mimeType = parentMimeType;
}
String representationUrl = null;
long indexStart = -1;
long indexEnd = -1;
long initializationStart = -1;
long initializationEnd = -1;
int numChannels = -1;
List<Segment> segmentList = null;
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
xpp.next();
representationUrl = xpp.getText();
} else if (isStartTag(xpp, "AudioChannelConfiguration")) {
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
} else if (isStartTag(xpp, "SegmentBase")) {
String[] indexRange = xpp.getAttributeValue(null, "indexRange").split("-");
indexStart = Long.parseLong(indexRange[0]);
indexEnd = Long.parseLong(indexRange[1]);
} else if (isStartTag(xpp, "SegmentList")) {
segmentList = parseRepresentationSegmentList(xpp, segmentTimelineList);
} else if (isStartTag(xpp, "Initialization")) {
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
initializationStart = Long.parseLong(indexRange[0]);
initializationEnd = Long.parseLong(indexRange[1]);
}
} while (!isEndTag(xpp, "Representation"));
Uri uri = Uri.parse(representationUrl);
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
bandwidth);
if (segmentList == null) {
return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED,
initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
periodDuration);
} else {
return new SegmentedRepresentation(contentId, format, uri, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
}
}
private List<Segment> parseRepresentationSegmentList(XmlPullParser xpp,
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
List<Segment> segmentList = new ArrayList<Segment>();
int i = 0;
do {
xpp.next();
if (isStartTag(xpp, "Initialization")) {
String url = xpp.getAttributeValue(null, "sourceURL");
String[] indexRange = xpp.getAttributeValue(null, "range").split("-");
long initializationStart = Long.parseLong(indexRange[0]);
long initializationEnd = Long.parseLong(indexRange[1]);
segmentList.add(new Segment.Initialization(url, initializationStart, initializationEnd));
} else if (isStartTag(xpp, "SegmentURL")) {
String url = xpp.getAttributeValue(null, "media");
String mediaRange = xpp.getAttributeValue(null, "mediaRange");
long sequenceNumber = segmentTimelineList.get(i).sequenceNumber;
long duration = segmentTimelineList.get(i).duration;
i++;
if (mediaRange != null) {
String[] mediaRangeArray = xpp.getAttributeValue(null, "mediaRange").split("-");
long mediaStart = Long.parseLong(mediaRangeArray[0]);
segmentList.add(new Segment.Media(url, mediaStart, sequenceNumber, duration));
} else {
segmentList.add(new Segment.Media(url, sequenceNumber, duration));
}
}
} while (!isEndTag(xpp, "SegmentList"));
return segmentList;
}
protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
}
protected static boolean isStartTag(XmlPullParser xpp, String name)
throws XmlPullParserException {
return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
}
protected static int parseInt(XmlPullParser xpp, String name) {
String value = xpp.getAttributeValue(null, name);
return value == null ? -1 : Integer.parseInt(value);
}
protected static long parseLong(XmlPullParser xpp, String name) {
return parseLong(xpp, name, -1);
}
protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Long.parseLong(value);
}
private long parseDurationMs(XmlPullParser xpp, String name) {
return parseDurationMs(xpp, name, -1);
}
private 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;
}
}