Add support for SegmentTemplate and SegmentList mpds.

Misc Notes:
- Removed content type filters because some of third parties
  don't set content type.
This commit is contained in:
Oliver Woodman 2014-07-18 14:30:30 +01:00
parent 87461821fe
commit 62d17cabf0
14 changed files with 485 additions and 387 deletions

View File

@ -52,7 +52,7 @@ public class SampleChooserActivity extends Activity {
sampleAdapter.addAll((Object[]) Samples.SIMPLE);
sampleAdapter.add(new Header("YouTube DASH"));
sampleAdapter.addAll((Object[]) Samples.YOUTUBE_DASH_MP4);
sampleAdapter.add(new Header("Widevine DASH GTS"));
sampleAdapter.add(new Header("Widevine GTS DASH"));
sampleAdapter.addAll((Object[]) Samples.WIDEVINE_GTS);
sampleAdapter.add(new Header("SmoothStreaming"));
sampleAdapter.addAll((Object[]) Samples.SMOOTHSTREAMING);

View File

@ -129,10 +129,6 @@ package com.google.android.exoplayer.demo;
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
new Sample("WV: 30s license duration", "f9a34cab7b05881a",
"http://dash.edgesuite.net/digitalprimates/fraunhofer/480p_video/heaac_2_0_with_video/ElephantsDream/elephants_dream_480p_heaac2_0.mpd", DemoUtil.TYPE_DASH_VOD, false, true),
};
public static final Sample[] MISC = new Sample[] {

View File

@ -160,8 +160,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
}
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource;
String mimeType = videoRepresentations[0].format.mimeType;
if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
@ -192,8 +191,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
audioChunkSource = null;
audioRenderer = null;
} else {
DataSource audioDataSource = new HttpDataSource(userAgent,
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
audioTrackNames = new String[audioRepresentationsList.size()];
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();

View File

@ -150,8 +150,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
}
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
@ -173,8 +172,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else {
audioTrackNames = new String[audioStreamElementCount];
ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount];
DataSource audioDataSource = new HttpDataSource(userAgent,
HttpDataSource.REJECT_PAYWALL_TYPES, bandwidthMeter);
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator();
audioStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) {
@ -204,8 +202,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
} else {
textTrackNames = new String[textStreamElementCount];
ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount];
DataSource ttmlDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource ttmlDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator();
textStreamElementCount = 0;
for (int i = 0; i < manifest.streamElements.length; i++) {

View File

@ -115,8 +115,7 @@ import java.util.ArrayList;
videoRepresentationsList.toArray(videoRepresentations);
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
@ -125,8 +124,7 @@ import java.util.ArrayList;
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,

View File

@ -115,8 +115,7 @@ import java.util.ArrayList;
}
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(url, manifest,
videoStreamElementIndex, videoTrackIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
@ -126,8 +125,7 @@ import java.util.ArrayList;
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 0, mainHandler, playerActivity, 50);
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, HttpDataSource.REJECT_PAYWALL_TYPES,
bandwidthMeter);
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(url, manifest,
audioStreamElementIndex, new int[] {0}, audioDataSource,
new FormatEvaluator.FixedEvaluator());

View File

@ -309,7 +309,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
}
MediaFormat mediaFormat = mediaChunk.getMediaFormat();
if (downstreamMediaFormat == null || !downstreamMediaFormat.equals(mediaFormat)) {
if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat)) {
chunkSource.getMaxVideoDimensions(mediaFormat);
formatHolder.format = mediaFormat;
formatHolder.drmInitData = mediaChunk.getPsshInfo();

View File

@ -74,7 +74,7 @@ public class DashMp4ChunkSource implements ChunkSource {
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].periodDuration * 1000);
representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation();
int maxWidth = 0;
int maxHeight = 0;
@ -198,7 +198,7 @@ public class DashMp4ChunkSource implements ChunkSource {
RangedUri requestUri;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request at once.
// the two requests together to request both at once.
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV;
requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) {

View File

@ -70,7 +70,7 @@ public class DashWebmChunkSource implements ChunkSource {
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(
representations[0].format.mimeType, representations[0].periodDuration * 1000);
representations[0].format.mimeType, representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation();
int maxWidth = 0;
int maxHeight = 0;

View File

@ -17,7 +17,10 @@ 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.dash.mpd.SegmentBase.SegmentList;
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.MimeTypes;
@ -39,11 +42,6 @@ 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 {
// Note: Does not support the date part of ISO 8601
@ -60,6 +58,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
}
}
// MPD parsing.
/**
* Parses a manifest from the provided {@link InputStream}.
*
@ -86,96 +86,69 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
}
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId, Uri parentBaseUrl) throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
long duration = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTime = parseDurationMs(xpp, "minBufferTime");
String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTimeMs = 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;
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
List<Period> periods = new ArrayList<Period>();
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, parentBaseUrl);
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, duration));
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
}
} while (!isEndTag(xpp, "MPD"));
return new MediaPresentationDescription(duration, minBufferTime, dynamic, minUpdateTime,
return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
periods);
}
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri parentBaseUrl,
long mediaPresentationDuration) throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
long start = parseDurationMs(xpp, "start", 0);
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
long startMs = parseDurationMs(xpp, "start", 0);
long durationMs = parseDurationMs(xpp, "duration", mpdDurationMs);
SegmentBase segmentBase = null;
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, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, parentBaseUrl);
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, start, duration,
segmentTimelineList));
adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs,
segmentBase));
} else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, null);
} else if (isStartTag(xpp, "SegmentList")) {
segmentStartNumber = parseInt(xpp, "startNumber");
segmentTimescale = parseInt(xpp, "timescale");
presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
segmentTimelineList = parsePeriodSegmentList(xpp, segmentStartNumber);
segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs);
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs);
}
} while (!isEndTag(xpp, "Period"));
return new Period(id, start, duration, adaptationSets, segmentTimelineList,
segmentStartNumber, segmentTimescale, presentationTimeOffset);
return new Period(id, startMs, durationMs, adaptationSets);
}
private List<Segment.Timeline> parsePeriodSegmentList(
XmlPullParser xpp, long segmentStartNumber) throws XmlPullParserException, IOException {
List<Segment.Timeline> segmentTimelineList = new ArrayList<Segment.Timeline>();
// AdaptationSet parsing.
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, Uri parentBaseUrl,
long periodStart, long periodDuration, List<Segment.Timeline> segmentTimelineList)
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl,
long periodStartMs, long periodDurationMs, SegmentBase segmentBase)
throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
int id = -1;
// TODO: Correctly handle other common attributes and elements. See 23009-1 Table 9.
String mimeType = xpp.getAttributeValue(null, "mimeType");
int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
int id = -1;
List<ContentProtection> contentProtections = null;
List<Representation> representations = new ArrayList<Representation>();
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, parentBaseUrl);
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>();
@ -186,17 +159,62 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
} else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStart,
periodDuration, mimeType, segmentTimelineList);
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
periodDurationMs, mimeType, segmentBase);
contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
representations.add(representation);
} else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
} else if (isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
periodDurationMs);
}
} while (!isEndTag(xpp, "AdaptationSet"));
return new AdaptationSet(id, contentType, representations, contentProtections);
}
private int parseAdaptationSetType(String contentType) {
return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN
: MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO
: MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO
: MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? AdaptationSet.TYPE_TEXT
: AdaptationSet.TYPE_UNKNOWN;
}
private int parseAdaptationSetTypeFromMimeType(String mimeType) {
return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN
: MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO
: MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO
: MimeTypes.isText(mimeType) || MimeTypes.isTtml(mimeType) ? AdaptationSet.TYPE_TEXT
: AdaptationSet.TYPE_UNKNOWN;
}
/**
* Checks two adaptation set types for consistency, returning the consistent type, or throwing an
* {@link IllegalStateException} if the types are inconsistent.
* <p>
* Two types are consistent if they are equal, or if one is {@link AdaptationSet#TYPE_UNKNOWN}.
* Where one of the types is {@link AdaptationSet#TYPE_UNKNOWN}, the other is returned.
*
* @param firstType The first type.
* @param secondType The second type.
* @return The consistent type.
*/
private int checkAdaptationSetTypeConsistency(int firstType, int secondType) {
if (firstType == AdaptationSet.TYPE_UNKNOWN) {
return secondType;
} else if (secondType == AdaptationSet.TYPE_UNKNOWN) {
return firstType;
} else {
Assertions.checkState(firstType == secondType);
return firstType;
}
}
/**
* Parses a ContentProtection element.
*
@ -209,90 +227,194 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return new ContentProtection(schemeUriId, null);
}
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri parentBaseUrl,
long periodStart, long periodDuration, String parentMimeType,
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
// Representation parsing.
private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
long periodStartMs, long periodDurationMs, String mimeType, SegmentBase segmentBase)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth");
int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
int width = parseInt(xpp, "width");
int height = parseInt(xpp, "height");
mimeType = parseString(xpp, "mimeType", mimeType);
String mimeType = xpp.getAttributeValue(null, "mimeType");
if (mimeType == null) {
mimeType = parentMimeType;
}
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")) {
baseUrl = parseBaseUrl(xpp, parentBaseUrl);
baseUrl = parseBaseUrl(xpp, baseUrl);
} 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]);
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
} 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]);
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
} else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
periodDurationMs);
}
} while (!isEndTag(xpp, "Representation"));
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
bandwidth);
if (segmentList == null) {
return new Representation(contentId, -1, format, baseUrl, DataSpec.LENGTH_UNBOUNDED,
initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
periodDuration);
} else {
return new SegmentedRepresentation(contentId, format, baseUrl, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
}
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, -1, format,
segmentBase);
}
private List<Segment> parseRepresentationSegmentList(XmlPullParser xpp,
List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
List<Segment> segmentList = new ArrayList<Segment>();
int i = 0;
// SegmentBase, SegmentList and SegmentTemplate parsing.
private SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl,
SingleSegmentBase parent) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long indexStart = parent != null ? parent.indexStart : 0;
long indexLength = parent != null ? parent.indexLength : -1;
String indexRangeText = xpp.getAttributeValue(null, "indexRange");
if (indexRangeText != null) {
String[] indexRange = indexRangeText.split("-");
indexStart = Long.parseLong(indexRange[0]);
indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;
}
RangedUri initialization = parent != null ? parent.initialization : null;
do {
xpp.next();
if (isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
}
} while (!isEndTag(xpp, "SegmentBase"));
return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
indexStart, indexLength);
}
private SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent,
long periodDuration) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0);
RangedUri initialization = null;
List<SegmentTimelineElement> timeline = null;
List<RangedUri> segments = null;
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));
initialization = parseInitialization(xpp, baseUrl);
} else if (isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp);
} 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));
if (segments == null) {
segments = new ArrayList<RangedUri>();
}
segments.add(parseSegmentUrl(xpp, baseUrl));
}
} while (!isEndTag(xpp, "SegmentList"));
return segmentList;
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
timeline = timeline != null ? timeline : parent.segmentTimeline;
segments = segments != null ? segments : parent.mediaSegments;
}
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration,
startNumber, duration, timeline, segments);
}
private SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl,
SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 0);
UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media",
parent != null ? parent.mediaTemplate : null);
UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization",
parent != null ? parent.initializationTemplate : null);
RangedUri initialization = null;
List<SegmentTimelineElement> timeline = null;
do {
xpp.next();
if (isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp, baseUrl);
} else if (isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp);
}
} while (!isEndTag(xpp, "SegmentTemplate"));
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
timeline = timeline != null ? timeline : parent.segmentTimeline;
}
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration,
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
}
private List<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
throws XmlPullParserException, IOException {
List<SegmentTimelineElement> segmentTimeline = new ArrayList<SegmentTimelineElement>();
long elapsedTime = 0;
do {
xpp.next();
if (isStartTag(xpp, "S")) {
elapsedTime = parseLong(xpp, "t", elapsedTime);
long duration = parseLong(xpp, "d");
int count = 1 + parseInt(xpp, "r", 0);
for (int i = 0; i < count; i++) {
segmentTimeline.add(new SegmentTimelineElement(elapsedTime, duration));
elapsedTime += duration;
}
}
} while (!isEndTag(xpp, "SegmentTimeline"));
return segmentTimeline;
}
private UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name,
UrlTemplate defaultValue) {
String valueString = xpp.getAttributeValue(null, name);
if (valueString != null) {
return UrlTemplate.compile(valueString);
}
return defaultValue;
}
private RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) {
return parseRangedUrl(xpp, baseUrl, "sourceURL", "range");
}
private RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) {
return parseRangedUrl(xpp, baseUrl, "media", "mediaRange");
}
private RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute,
String rangeAttribute) {
String urlText = xpp.getAttributeValue(null, urlAttribute);
long rangeStart = 0;
long rangeLength = -1;
String rangeText = xpp.getAttributeValue(null, rangeAttribute);
if (rangeText != null) {
String[] rangeTextArray = rangeText.split("-");
rangeStart = Long.parseLong(rangeTextArray[0]);
rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;
}
return new RangedUri(baseUrl, urlText, rangeStart, rangeLength);
}
// Utility methods.
protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
}
@ -302,21 +424,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
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) {
private static long parseDurationMs(XmlPullParser xpp, String name) {
return parseDurationMs(xpp, name, -1);
}
@ -339,54 +447,38 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return defaultValue;
}
private static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
throws XmlPullParserException, IOException {
xpp.next();
String newBaseUrlText = xpp.getText();
Uri newBaseUri = Uri.parse(newBaseUrlText);
if (newBaseUri.isAbsolute()) {
return newBaseUri;
} else {
return parentBaseUrl.buildUpon().appendEncodedPath(newBaseUrlText).build();
if (!newBaseUri.isAbsolute()) {
newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText);
}
return newBaseUri;
}
private static int parseAdaptationSetType(String contentType) {
return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN
: MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO
: MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO
: MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? AdaptationSet.TYPE_TEXT
: AdaptationSet.TYPE_UNKNOWN;
protected static int parseInt(XmlPullParser xpp, String name) {
return parseInt(xpp, name, -1);
}
private static int parseAdaptationSetTypeFromMimeType(String mimeType) {
return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN
: MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO
: MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO
: MimeTypes.isText(mimeType) || MimeTypes.isTtml(mimeType) ? AdaptationSet.TYPE_TEXT
: AdaptationSet.TYPE_UNKNOWN;
protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Integer.parseInt(value);
}
/**
* Checks two adaptation set types for consistency, returning the consistent type, or throwing an
* {@link IllegalStateException} if the types are inconsistent.
* <p>
* Two types are consistent if they are equal, or if one is {@link AdaptationSet#TYPE_UNKNOWN}.
* Where one of the types is {@link AdaptationSet#TYPE_UNKNOWN}, the other is returned.
*
* @param firstType The first type.
* @param secondType The second type.
* @return The consistent type.
*/
private static int checkAdaptationSetTypeConsistency(int firstType, int secondType) {
if (firstType == AdaptationSet.TYPE_UNKNOWN) {
return secondType;
} else if (secondType == AdaptationSet.TYPE_UNKNOWN) {
return firstType;
} else {
Assertions.checkState(firstType == secondType);
return firstType;
}
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);
}
protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : value;
}
}

View File

@ -23,46 +23,37 @@ import java.util.List;
*/
public final class Period {
/**
* The period identifier, if one exists.
*/
public final String id;
public final long start;
/**
* The start time of the period in milliseconds.
*/
public final long startMs;
public final long duration;
/**
* The duration of the period in milliseconds, or -1 if the duration is unknown.
*/
public final long durationMs;
/**
* The adaptation sets belonging to the period.
*/
public final List<AdaptationSet> adaptationSets;
public final List<Segment.Timeline> segmentList;
public final int segmentStartNumber;
public final int segmentTimescale;
public final long presentationTimeOffset;
/**
* @param id The period identifier. May be null.
* @param start The start time of the period in milliseconds.
* @param duration The duration of the period in milliseconds, or -1 if the duration is unknown.
* @param adaptationSets The adaptation sets belonging to the period.
*/
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
this(id, start, duration, adaptationSets, null, 0, 0, 0);
}
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets,
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale) {
this(id, start, duration, adaptationSets, segmentList, segmentStartNumber, segmentTimescale, 0);
}
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets,
List<Segment.Timeline> segmentList, int segmentStartNumber, int segmentTimescale,
long presentationTimeOffset) {
this.id = id;
this.start = start;
this.duration = duration;
this.startMs = start;
this.durationMs = duration;
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
if (segmentList != null) {
this.segmentList = Collections.unmodifiableList(segmentList);
} else {
this.segmentList = null;
}
this.segmentStartNumber = segmentStartNumber;
this.segmentTimescale = segmentTimescale;
this.presentationTimeOffset = presentationTimeOffset;
}
}

View File

@ -17,13 +17,15 @@ package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.dash.DashSegmentIndex;
import com.google.android.exoplayer.dash.mpd.SegmentBase.MultiSegmentBase;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
import android.net.Uri;
/**
* A flat version of a DASH representation.
* A DASH representation.
*/
public class Representation {
public abstract class Representation {
/**
* Identifies the piece of content to which this {@link Representation} belongs.
@ -34,7 +36,7 @@ public class Representation {
public final String contentId;
/**
* Identifies the revision of the {@link Representation}.
* Identifies the revision of the content.
* <p>
* If the media for a given ({@link #contentId} can change over time without a change to the
* {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
@ -44,40 +46,62 @@ public class Representation {
public final long revisionId;
/**
* The format in which the {@link Representation} is encoded.
* The format of the representation.
*/
public final Format format;
public final long contentLength;
/**
* The start time of the enclosing period in milliseconds since the epoch.
*/
public final long periodStartMs;
public final long initializationStart;
/**
* The duration of the enclosing period in milliseconds.
*/
public final long periodDurationMs;
public final long initializationEnd;
/**
* The offset of the presentation timestamps in the media stream relative to media time.
*/
public final long presentationTimeOffsetMs;
public final long indexStart;
private final RangedUri initializationUri;
public final long indexEnd;
/**
* Constructs a new instance.
*
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param segmentBase A segment base element for the representation.
* @return The constructed instance.
*/
public static Representation newInstance(long periodStartMs, long periodDurationMs,
String contentId, long revisionId, Format format, SegmentBase segmentBase) {
if (segmentBase instanceof SingleSegmentBase) {
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
format, (SingleSegmentBase) segmentBase, -1);
} else if (segmentBase instanceof MultiSegmentBase) {
return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
format, (MultiSegmentBase) segmentBase);
} else {
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+ "MultiSegmentBase");
}
}
public final long periodStart;
public final long periodDuration;
public final Uri uri;
public Representation(String contentId, long revisionId, Format format, Uri uri,
long contentLength, long initializationStart, long initializationEnd, long indexStart,
long indexEnd, long periodStart, long periodDuration) {
private Representation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SegmentBase segmentBase) {
this.periodStartMs = periodStartMs;
this.periodDurationMs = periodDurationMs;
this.contentId = contentId;
this.revisionId = revisionId;
this.format = format;
this.contentLength = contentLength;
this.initializationStart = initializationStart;
this.initializationEnd = initializationEnd;
this.indexStart = indexStart;
this.indexEnd = indexEnd;
this.periodStart = periodStart;
this.periodDuration = periodDuration;
this.uri = uri;
initializationUri = segmentBase.getInitialization(this);
presentationTimeOffsetMs = (segmentBase.presentationTimeOffset * 1000) / segmentBase.timescale;
}
/**
@ -87,8 +111,7 @@ public class Representation {
* @return A {@link RangedUri} defining the location of the initialization data, or null.
*/
public RangedUri getInitializationUri() {
return new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1);
return initializationUri;
}
/**
@ -97,9 +120,7 @@ public class Representation {
*
* @return The location of the segment index, or null.
*/
public RangedUri getIndexUri() {
return new RangedUri(uri, null, indexStart, indexEnd - indexStart + 1);
}
public abstract RangedUri getIndexUri();
/**
* Gets a segment index, if the representation is able to provide one directly. Null if the
@ -107,9 +128,7 @@ public class Representation {
*
* @return The segment index, or null.
*/
public DashSegmentIndex getIndex() {
return null;
}
public abstract DashSegmentIndex getIndex();
/**
* Generates a cache key for the {@link Representation}, in the format
@ -121,4 +140,143 @@ public class Representation {
return contentId + "." + format.id + "." + revisionId;
}
/**
* A DASH representation consisting of a single segment.
*/
public static class SingleSegmentRepresentation extends Representation {
/**
* The {@link Uri} of the single segment.
*/
public final Uri uri;
/**
* The content length, or -1 if unknown.
*/
public final long contentLength;
private final RangedUri indexUri;
/**
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param uri The uri of the media.
* @param initializationStart The offset of the first byte of initialization data.
* @param initializationEnd The offset of the last byte of initialization data.
* @param indexStart The offset of the first byte of index data.
* @param indexEnd The offset of the last byte of index data.
* @param contentLength The content length, or -1 if unknown.
*/
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
String contentId, long revisionId, Format format, Uri uri, long initializationStart,
long initializationEnd, long indexStart, long indexEnd, long contentLength) {
RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1);
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
indexEnd - indexStart + 1);
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
format, segmentBase, contentLength);
}
/**
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param segmentBase The segment base underlying the representation.
* @param contentLength The content length, or -1 if unknown.
*/
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.uri = segmentBase.uri;
this.indexUri = segmentBase.getIndex();
this.contentLength = contentLength;
}
@Override
public RangedUri getIndexUri() {
return indexUri;
}
@Override
public DashSegmentIndex getIndex() {
return null;
}
}
/**
* A DASH representation consisting of multiple segments.
*/
public static class MultiSegmentRepresentation extends Representation
implements DashSegmentIndex {
private final MultiSegmentBase segmentBase;
/**
* @param periodStartMs The start time of the enclosing period in milliseconds.
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
* duration is unknown.
* @param contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content.
* @param format The format of the representation.
* @param segmentBase The segment base underlying the representation.
*/
public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
long revisionId, Format format, MultiSegmentBase segmentBase) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
this.segmentBase = segmentBase;
}
@Override
public RangedUri getIndexUri() {
return null;
}
@Override
public DashSegmentIndex getIndex() {
return this;
}
// DashSegmentIndex implementation.
@Override
public RangedUri getSegmentUrl(int segmentIndex) {
return segmentBase.getSegmentUrl(this, segmentIndex);
}
@Override
public int getSegmentNum(long timeUs) {
return segmentBase.getSegmentNum(timeUs);
}
@Override
public long getTimeUs(int segmentIndex) {
return segmentBase.getSegmentTimeUs(segmentIndex);
}
@Override
public long getDurationUs(int segmentIndex) {
return segmentBase.getSegmentDurationUs(segmentIndex);
}
@Override
public int getFirstSegmentNum() {
return segmentBase.getFirstSegmentNum();
}
@Override
public int getLastSegmentNum() {
return segmentBase.getLastSegmentNum();
}
}
}

View File

@ -1,81 +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;
/**
* Represents a particular segment in a Representation.
*
*/
public abstract class Segment {
public final String relativeUri;
public final long sequenceNumber;
public final long duration;
public Segment(String relativeUri, long sequenceNumber, long duration) {
this.relativeUri = relativeUri;
this.sequenceNumber = sequenceNumber;
this.duration = duration;
}
/**
* Represents a timeline segment from the MPD's SegmentTimeline list.
*/
public static class Timeline extends Segment {
public Timeline(long sequenceNumber, long duration) {
super(null, sequenceNumber, duration);
}
}
/**
* Represents an initialization segment.
*/
public static class Initialization extends Segment {
public final long initializationStart;
public final long initializationEnd;
public Initialization(String relativeUri, long initializationStart,
long initializationEnd) {
super(relativeUri, -1, -1);
this.initializationStart = initializationStart;
this.initializationEnd = initializationEnd;
}
}
/**
* Represents a media segment.
*/
public static class Media extends Segment {
public final long mediaStart;
public Media(String relativeUri, long sequenceNumber, long duration) {
this(relativeUri, 0, sequenceNumber, duration);
}
public Media(String uri, long mediaStart, long sequenceNumber, long duration) {
super(uri, sequenceNumber, duration);
this.mediaStart = mediaStart;
}
}
}

View File

@ -1,49 +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.chunk.Format;
import com.google.android.exoplayer.upstream.DataSpec;
import android.net.Uri;
import java.util.List;
/**
* Represents a DASH Representation which uses the SegmentList structure (i.e. it has a list of
* Segment URLs instead of a single URL).
*/
public class SegmentedRepresentation extends Representation {
private List<Segment> segmentList;
public SegmentedRepresentation(String contentId, Format format, Uri uri, long initializationStart,
long initializationEnd, long indexStart, long indexEnd, long periodStart, long periodDuration,
List<Segment> segmentList) {
super(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration);
this.segmentList = segmentList;
}
public int getNumSegments() {
return segmentList.size();
}
public Segment getSegment(int i) {
return segmentList.get(i);
}
}