/*
* 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.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import android.util.Base64;
import android.util.Pair;
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.LinkedList;
import java.util.List;
import java.util.UUID;
/**
* Parses SmoothStreaming client manifests.
*
* @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
* IIS Smooth Streaming Client Manifest Format</a>
*/
public class SmoothStreamingManifestParser {
private final XmlPullParserFactory xmlParserFactory;
public SmoothStreamingManifestParser() {
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.
* @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream.
* @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest.
*/
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
IOException, ParserException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, inputEncoding);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) {
throw new ParserException(e);
}
}
/**
* Thrown if a required field is missing.
*/
public static class MissingFieldException extends ParserException {
public MissingFieldException(String fieldName) {
super("Missing required field: " + fieldName);
}
}
/**
* A base class for parsers that parse components of a smooth streaming manifest.
*/
private static abstract class ElementParser {
private final String tag;
private final ElementParser parent;
private final List<Pair<String, Object>> normalizedAttributes;
public ElementParser(String tag, ElementParser parent) {
this.tag = tag;
this.parent = parent;
this.normalizedAttributes = new LinkedList<Pair<String, Object>>();
}
public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException,
ParserException {
String tagName;
boolean foundStartTag = false;
int skippingElementDepth = 0;
while (true) {
int eventType = xmlParser.getEventType();
switch (eventType) {
case XmlPullParser.START_TAG:
tagName = xmlParser.getName();
if (tag.equals(tagName)) {
foundStartTag = true;
parseStartTag(xmlParser);
} else if (foundStartTag) {
if (skippingElementDepth > 0) {
skippingElementDepth++;
} else if (handleChildInline(tagName)) {
parseStartTag(xmlParser);
} else {
ElementParser childElementParser = newChildParser(this, tagName);
if (childElementParser == null) {
skippingElementDepth = 1;
} else {
addChild(childElementParser.parse(xmlParser));
}
}
}
break;
case XmlPullParser.TEXT:
if (foundStartTag && skippingElementDepth == 0) {
parseText(xmlParser);
}
break;
case XmlPullParser.END_TAG:
if (foundStartTag) {
if (skippingElementDepth > 0) {
skippingElementDepth--;
} else {
tagName = xmlParser.getName();
parseEndTag(xmlParser);
if (!handleChildInline(tagName)) {
return build();
}
}
}
break;
case XmlPullParser.END_DOCUMENT:
return null;
default:
// Do nothing.
break;
}
xmlParser.next();
}
}
private ElementParser newChildParser(ElementParser parent, String name) {
if (TrackElementParser.TAG.equals(name)) {
return new TrackElementParser(parent);
} else if (ProtectionElementParser.TAG.equals(name)) {
return new ProtectionElementParser(parent);
} else if (StreamElementParser.TAG.equals(name)) {
return new StreamElementParser(parent);
}
return null;
}
/**
* Stash an attribute that may be normalized at this level. In other words, an attribute that
* may have been pulled up from the child elements because its value was the same in all
* children.
* <p>
* Stashing an attribute allows child element parsers to retrieve the values of normalized
* attributes using {@link #getNormalizedAttribute(String)}.
*
* @param key The name of the attribute.
* @param value The value of the attribute.
*/
protected final void putNormalizedAttribute(String key, Object value) {
normalizedAttributes.add(Pair.create(key, value));
}
/**
* Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with
* the provided name, the parent element parser will be queried, and so on up the chain.
*
* @param key The name of the attribute.
* @return The stashed value, or null if the attribute was not be found.
*/
protected final Object getNormalizedAttribute(String key) {
for (int i = 0; i < normalizedAttributes.size(); i++) {
Pair<String, Object> pair = normalizedAttributes.get(i);
if (pair.first.equals(key)) {
return pair.second;
}
}
return parent == null ? null : parent.getNormalizedAttribute(key);
}
/**
* Whether this {@link ElementParser} parses a child element inline.
*
* @param tagName The name of the child element.
* @return Whether the child is parsed inline.
*/
protected boolean handleChildInline(String tagName) {
return false;
}
/**
* @param xmlParser The underlying {@link XmlPullParser}
* @throws ParserException
*/
protected void parseStartTag(XmlPullParser xmlParser) throws ParserException {
// Do nothing.
}
/**
* @param xmlParser The underlying {@link XmlPullParser}
* @throws ParserException
*/
protected void parseText(XmlPullParser xmlParser) throws ParserException {
// Do nothing.
}
/**
* @param xmlParser The underlying {@link XmlPullParser}
* @throws ParserException
*/
protected void parseEndTag(XmlPullParser xmlParser) throws ParserException {
// Do nothing.
}
/**
* @param parsedChild A parsed child object.
*/
protected void addChild(Object parsedChild) {
// Do nothing.
}
protected abstract Object build();
protected final String parseRequiredString(XmlPullParser parser, String key)
throws MissingFieldException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
return value;
} else {
throw new MissingFieldException(key);
}
}
protected final int parseInt(XmlPullParser parser, String key, int defaultValue)
throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new ParserException(e);
}
} else {
return defaultValue;
}
}
protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new ParserException(e);
}
} else {
throw new MissingFieldException(key);
}
}
protected final long parseLong(XmlPullParser parser, String key, long defaultValue)
throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
throw new ParserException(e);
}
} else {
return defaultValue;
}
}
protected final long parseRequiredLong(XmlPullParser parser, String key)
throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
throw new ParserException(e);
}
} else {
throw new MissingFieldException(key);
}
}
}
private static class SmoothStreamMediaParser extends ElementParser {
public static final String TAG = "SmoothStreamingMedia";
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_DURATION = "Duration";
private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
private int majorVersion;
private int minorVersion;
private long timeScale;
private long duration;
private int lookAheadCount;
private ProtectionElement protectionElement;
private List<StreamElement> streamElements;
public SmoothStreamMediaParser(ElementParser parent) {
super(TAG, parent);
lookAheadCount = -1;
protectionElement = null;
streamElements = new LinkedList<StreamElement>();
}
@Override
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);
duration = parseRequiredLong(parser, KEY_DURATION);
lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1);
putNormalizedAttribute(KEY_TIME_SCALE, timeScale);
}
@Override
public void addChild(Object child) {
if (child instanceof StreamElement) {
streamElements.add((StreamElement) child);
} else if (child instanceof ProtectionElement) {
Assertions.checkState(protectionElement == null);
protectionElement = (ProtectionElement) child;
}
}
@Override
public Object build() {
StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
streamElements.toArray(streamElementArray);
return new SmoothStreamingManifest(majorVersion, minorVersion, timeScale, duration,
lookAheadCount, protectionElement, streamElementArray);
}
}
private static class ProtectionElementParser extends ElementParser {
public static final String TAG = "Protection";
public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";
public static final String KEY_SYSTEM_ID = "SystemID";
private boolean inProtectionHeader;
private UUID uuid;
private byte[] initData;
public ProtectionElementParser(ElementParser parent) {
super(TAG, parent);
}
@Override
public boolean handleChildInline(String tag) {
return TAG_PROTECTION_HEADER.equals(tag);
}
@Override
public void parseStartTag(XmlPullParser parser) {
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
inProtectionHeader = true;
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
uuid = UUID.fromString(uuidString);
}
}
@Override
public void parseText(XmlPullParser parser) {
if (inProtectionHeader) {
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
}
}
@Override
public void parseEndTag(XmlPullParser parser) {
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
inProtectionHeader = false;
}
}
@Override
public Object build() {
return new ProtectionElement(uuid, initData);
}
}
private static class StreamElementParser extends ElementParser {
public static final String TAG = "StreamIndex";
private static final String TAG_STREAM_FRAGMENT = "c";
private static final String KEY_TYPE = "Type";
private static final String KEY_TYPE_AUDIO = "audio";
private static final String KEY_TYPE_VIDEO = "video";
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";
private static final String KEY_MAX_HEIGHT = "MaxHeight";
private static final String KEY_DISPLAY_WIDTH = "DisplayWidth";
private static final String KEY_DISPLAY_HEIGHT = "DisplayHeight";
private static final String KEY_LANGUAGE = "Language";
private static final String KEY_TIME_SCALE = "TimeScale";
private static final String KEY_FRAGMENT_DURATION = "d";
private static final String KEY_FRAGMENT_START_TIME = "t";
private final List<TrackElement> tracks;
private int type;
private String subType;
private long timeScale;
private String name;
private int qualityLevels;
private String url;
private int maxWidth;
private int maxHeight;
private int displayWidth;
private int displayHeight;
private String language;
private long[] startTimes;
private int chunkIndex;
private long previousChunkDuration;
public StreamElementParser(ElementParser parent) {
super(TAG, parent);
tracks = new LinkedList<TrackElement>();
}
@Override
public boolean handleChildInline(String tag) {
return TAG_STREAM_FRAGMENT.equals(tag);
}
@Override
public void parseStartTag(XmlPullParser parser) throws ParserException {
if (TAG_STREAM_FRAGMENT.equals(parser.getName())) {
parseStreamFragmentStartTag(parser);
} else {
parseStreamElementStartTag(parser);
}
}
private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
startTimes[chunkIndex] = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L);
if (startTimes[chunkIndex] == -1L) {
if (chunkIndex == 0) {
// Assume the track starts at t = 0.
startTimes[chunkIndex] = 0;
} else if (previousChunkDuration != -1L) {
// Infer the start time from the previous chunk's start time and duration.
startTimes[chunkIndex] = startTimes[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");
}
}
previousChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
chunkIndex++;
}
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
type = parseType(parser);
putNormalizedAttribute(KEY_TYPE, type);
if (type == StreamElement.TYPE_TEXT) {
subType = parseRequiredString(parser, KEY_SUB_TYPE);
} else {
subType = parser.getAttributeValue(null, KEY_SUB_TYPE);
}
name = parser.getAttributeValue(null, KEY_NAME);
qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1);
url = parseRequiredString(parser, KEY_URL);
maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1);
maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1);
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);
}
startTimes = new long[parseRequiredInt(parser, KEY_CHUNKS)];
}
private int parseType(XmlPullParser parser) throws ParserException {
String value = parser.getAttributeValue(null, KEY_TYPE);
if (value != null) {
if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) {
return StreamElement.TYPE_AUDIO;
} else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) {
return StreamElement.TYPE_VIDEO;
} else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) {
return StreamElement.TYPE_TEXT;
} else {
throw new ParserException("Invalid key value[" + value + "]");
}
}
throw new MissingFieldException(KEY_TYPE);
}
@Override
public void addChild(Object child) {
if (child instanceof TrackElement) {
tracks.add((TrackElement) child);
}
}
@Override
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);
}
}
private static class TrackElementParser extends ElementParser {
public static final String TAG = "QualityLevel";
private static final String KEY_INDEX = "Index";
private static final String KEY_BITRATE = "Bitrate";
private static final String KEY_CODEC_PRIVATE_DATA = "CodecPrivateData";
private static final String KEY_SAMPLING_RATE = "SamplingRate";
private static final String KEY_CHANNELS = "Channels";
private static final String KEY_BITS_PER_SAMPLE = "BitsPerSample";
private static final String KEY_PACKET_SIZE = "PacketSize";
private static final String KEY_AUDIO_TAG = "AudioTag";
private static final String KEY_FOUR_CC = "FourCC";
private static final String KEY_NAL_UNIT_LENGTH_FIELD = "NALUnitLengthField";
private static final String KEY_TYPE = "Type";
private static final String KEY_MAX_WIDTH = "MaxWidth";
private static final String KEY_MAX_HEIGHT = "MaxHeight";
private final List<byte[]> csd;
private int index;
private int bitrate;
private String fourCC;
private int profile;
private int level;
private int maxWidth;
private int maxHeight;
private int samplingRate;
private int channels;
private int packetSize;
private int audioTag;
private int bitPerSample;
private int nalUnitLengthField;
private String content;
public TrackElementParser(ElementParser parent) {
super(TAG, parent);
this.csd = new LinkedList<byte[]>();
}
@Override
public void parseStartTag(XmlPullParser parser) throws ParserException {
int type = (Integer) getNormalizedAttribute(KEY_TYPE);
content = null;
String value;
index = parseInt(parser, KEY_INDEX, -1);
bitrate = parseRequiredInt(parser, KEY_BITRATE);
nalUnitLengthField = parseInt(parser, KEY_NAL_UNIT_LENGTH_FIELD, 4);
if (type == StreamElement.TYPE_VIDEO) {
maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
} else {
maxHeight = -1;
maxWidth = -1;
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
}
if (type == StreamElement.TYPE_AUDIO) {
samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE);
channels = parseRequiredInt(parser, KEY_CHANNELS);
bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
} else {
samplingRate = -1;
channels = -1;
bitPerSample = -1;
packetSize = -1;
audioTag = -1;
}
value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);
if (value != null && value.length() > 0) {
byte[] codecPrivateData = hexStringToByteArray(value);
byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData);
if (split == null) {
csd.add(codecPrivateData);
} else {
for (int i = 0; i < split.length; i++) {
Pair<Integer, Integer> spsParameters = CodecSpecificDataUtil.parseSpsNalUnit(split[i]);
if (spsParameters != null) {
profile = spsParameters.first;
level = spsParameters.second;
}
csd.add(split[i]);
}
}
}
}
private byte[] hexStringToByteArray(String hexString) {
int length = hexString.length();
byte[] data = new byte[length / 2];
for (int i = 0; i < data.length; i++) {
int stringOffset = i * 2;
data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4)
+ Character.digit(hexString.charAt(stringOffset + 1), 16));
}
return data;
}
@Override
public void parseText(XmlPullParser parser) {
content = parser.getText();
}
@Override
public Object build() {
byte[][] csdArray = null;
if (!csd.isEmpty()) {
csdArray = new byte[csd.size()][];
csd.toArray(csdArray);
}
return new TrackElement(index, bitrate, fourCC, csdArray, profile, level, maxWidth, maxHeight,
samplingRate, channels, packetSize, audioTag, bitPerSample, nalUnitLengthField, content);
}
}
}