/*
* Copyright (C) 2016 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 androidx.media3.exoplayer.dash.manifest;
import static androidx.media3.exoplayer.dash.manifest.BaseUrl.DEFAULT_DVB_PRIORITY;
import static androidx.media3.exoplayer.dash.manifest.BaseUrl.DEFAULT_WEIGHT;
import static androidx.media3.exoplayer.dash.manifest.BaseUrl.PRIORITY_UNSET;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Pair;
import android.util.Xml;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.UriUtil;
import androidx.media3.common.util.Util;
import androidx.media3.common.util.XmlPullParserUtil;
import androidx.media3.exoplayer.dash.manifest.SegmentBase.SegmentList;
import androidx.media3.exoplayer.dash.manifest.SegmentBase.SegmentTemplate;
import androidx.media3.exoplayer.dash.manifest.SegmentBase.SegmentTimelineElement;
import androidx.media3.exoplayer.dash.manifest.SegmentBase.SingleSegmentBase;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.extractor.metadata.emsg.EventMessage;
import androidx.media3.extractor.mp4.PsshAtomUtil;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
/** A parser of media presentation description files. */
@UnstableApi
public class DashManifestParser extends DefaultHandler
implements ParsingLoadable.Parser<DashManifest> {
private static final String TAG = "MpdParser";
private static final Pattern FRAME_RATE_PATTERN = Pattern.compile("(\d+)(?:/(\d+))?");
private static final Pattern CEA_608_ACCESSIBILITY_PATTERN = Pattern.compile("CC([1-4])=.*");
private static final Pattern CEA_708_ACCESSIBILITY_PATTERN =
Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*");
/**
* Maps the value attribute of an AudioChannelConfiguration with schemeIdUri
* "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1, to a channel
* count.
*/
private static final int[] MPEG_CHANNEL_CONFIGURATION_MAPPING =
new int[] {
Format.NO_VALUE, 1, 2, 3, 4, 5, 6, 8, 2, 3, 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, 14
};
private final XmlPullParserFactory xmlParserFactory;
public DashManifestParser() {
try {
xmlParserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
}
}
// MPD parsing.
@Override
public DashManifest parse(Uri uri, InputStream inputStream) throws IOException {
try {
XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, null);
int eventType = xpp.next();
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
throw ParserException.createForMalformedManifest(
"inputStream does not contain a valid media presentation description",
/* cause= */ null);
}
return parseMediaPresentationDescription(xpp, uri);
} catch (XmlPullParserException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, /* cause= */ e);
}
}
protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, Uri documentBaseUri)
throws XmlPullParserException, IOException {
boolean dvbProfileDeclared =
isDvbProfileDeclared(parseProfiles(xpp, "profiles", new String[0]));
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", C.TIME_UNSET);
long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET);
long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET);
String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = "dynamic".equals(typeString);
long minUpdateTimeMs =
dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) : C.TIME_UNSET;
long timeShiftBufferDepthMs =
dynamic ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET;
long suggestedPresentationDelayMs =
dynamic ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET;
long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET);
ProgramInformation programInformation = null;
UtcTimingElement utcTiming = null;
Uri location = null;
ServiceDescriptionElement serviceDescription = null;
long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET;
BaseUrl documentBaseUrl =
new BaseUrl(
documentBaseUri.toString(),
/* serviceLocation= */ documentBaseUri.toString(),
dvbProfileDeclared ? DEFAULT_DVB_PRIORITY : PRIORITY_UNSET,
DEFAULT_WEIGHT);
ArrayList<BaseUrl> parentBaseUrls = Lists.newArrayList(documentBaseUrl);
List<Period> periods = new ArrayList<>();
ArrayList<BaseUrl> baseUrls = new ArrayList<>();
long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0;
boolean seenEarlyAccessPeriod = false;
boolean seenFirstBaseUrl = false;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrlAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs);
seenFirstBaseUrl = true;
}
baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls, dvbProfileDeclared));
} else if (XmlPullParserUtil.isStartTag(xpp, "ProgramInformation")) {
programInformation = parseProgramInformation(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) {
utcTiming = parseUtcTiming(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Location")) {
location = UriUtil.resolveToUri(documentBaseUri.toString(), xpp.nextText());
} else if (XmlPullParserUtil.isStartTag(xpp, "ServiceDescription")) {
serviceDescription = parseServiceDescription(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
Pair<Period, Long> periodWithDurationMs =
parsePeriod(
xpp,
!baseUrls.isEmpty() ? baseUrls : parentBaseUrls,
nextPeriodStartMs,
baseUrlAvailabilityTimeOffsetUs,
availabilityStartTime,
timeShiftBufferDepthMs,
dvbProfileDeclared);
Period period = periodWithDurationMs.first;
if (period.startMs == C.TIME_UNSET) {
if (dynamic) {
// This is an early access period. Ignore it. All subsequent periods must also be
// early access.
seenEarlyAccessPeriod = true;
} else {
throw ParserException.createForMalformedManifest(
"Unable to determine start of period " + periods.size(), /* cause= */ null);
}
} else {
long periodDurationMs = periodWithDurationMs.second;
nextPeriodStartMs =
periodDurationMs == C.TIME_UNSET ? C.TIME_UNSET : (period.startMs + periodDurationMs);
periods.add(period);
}
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "MPD"));
if (durationMs == C.TIME_UNSET) {
if (nextPeriodStartMs != C.TIME_UNSET) {
// If we know the end time of the final period, we can use it as the duration.
durationMs = nextPeriodStartMs;
} else if (!dynamic) {
throw ParserException.createForMalformedManifest(
"Unable to determine duration of static manifest.", /* cause= */ null);
}
}
if (periods.isEmpty()) {
throw ParserException.createForMalformedManifest("No periods found.", /* cause= */ null);
}
return buildMediaPresentationDescription(
availabilityStartTime,
durationMs,
minBufferTimeMs,
dynamic,
minUpdateTimeMs,
timeShiftBufferDepthMs,
suggestedPresentationDelayMs,
publishTimeMs,
programInformation,
utcTiming,
serviceDescription,
location,
periods);
}
protected DashManifest buildMediaPresentationDescription(
long availabilityStartTime,
long durationMs,
long minBufferTimeMs,
boolean dynamic,
long minUpdateTimeMs,
long timeShiftBufferDepthMs,
long suggestedPresentationDelayMs,
long publishTimeMs,
@Nullable ProgramInformation programInformation,
@Nullable UtcTimingElement utcTiming,
@Nullable ServiceDescriptionElement serviceDescription,
@Nullable Uri location,
List<Period> periods) {
return new DashManifest(
availabilityStartTime,
durationMs,
minBufferTimeMs,
dynamic,
minUpdateTimeMs,
timeShiftBufferDepthMs,
suggestedPresentationDelayMs,
publishTimeMs,
programInformation,
utcTiming,
serviceDescription,
location,
periods);
}
protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
String value = xpp.getAttributeValue(null, "value");
return buildUtcTimingElement(schemeIdUri, value);
}
protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) {
return new UtcTimingElement(schemeIdUri, value);
}
protected ServiceDescriptionElement parseServiceDescription(XmlPullParser xpp)
throws XmlPullParserException, IOException {
long targetOffsetMs = C.TIME_UNSET;
long minOffsetMs = C.TIME_UNSET;
long maxOffsetMs = C.TIME_UNSET;
float minPlaybackSpeed = C.RATE_UNSET;
float maxPlaybackSpeed = C.RATE_UNSET;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Latency")) {
targetOffsetMs = parseLong(xpp, "target", C.TIME_UNSET);
minOffsetMs = parseLong(xpp, "min", C.TIME_UNSET);
maxOffsetMs = parseLong(xpp, "max", C.TIME_UNSET);
} else if (XmlPullParserUtil.isStartTag(xpp, "PlaybackRate")) {
minPlaybackSpeed = parseFloat(xpp, "min", C.RATE_UNSET);
maxPlaybackSpeed = parseFloat(xpp, "max", C.RATE_UNSET);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ServiceDescription"));
return new ServiceDescriptionElement(
targetOffsetMs, minOffsetMs, maxOffsetMs, minPlaybackSpeed, maxPlaybackSpeed);
}
protected Pair<Period, Long> parsePeriod(
XmlPullParser xpp,
List<BaseUrl> parentBaseUrls,
long defaultStartMs,
long baseUrlAvailabilityTimeOffsetUs,
long availabilityStartTimeMs,
long timeShiftBufferDepthMs,
boolean dvbProfileDeclared)
throws XmlPullParserException, IOException {
@Nullable String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", defaultStartMs);
long periodStartUnixTimeMs =
availabilityStartTimeMs != C.TIME_UNSET ? availabilityStartTimeMs + startMs : C.TIME_UNSET;
long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
@Nullable SegmentBase segmentBase = null;
@Nullable Descriptor assetIdentifier = null;
List<AdaptationSet> adaptationSets = new ArrayList<>();
List<EventStream> eventStreams = new ArrayList<>();
ArrayList<BaseUrl> baseUrls = new ArrayList<>();
boolean seenFirstBaseUrl = false;
long segmentBaseAvailabilityTimeOffsetUs = C.TIME_UNSET;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrlAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs);
seenFirstBaseUrl = true;
}
baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls, dvbProfileDeclared));
} else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(
parseAdaptationSet(
xpp,
!baseUrls.isEmpty() ? baseUrls : parentBaseUrls,
segmentBase,
durationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
periodStartUnixTimeMs,
timeShiftBufferDepthMs,
dvbProfileDeclared));
} else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) {
eventStreams.add(parseEventStream(xpp));
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, /* parent= */ null);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBaseAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET);
segmentBase =
parseSegmentList(
xpp,
/* parent= */ null,
periodStartUnixTimeMs,
durationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBaseAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET);
segmentBase =
parseSegmentTemplate(
xpp,
/* parent= */ null,
ImmutableList.of(),
periodStartUnixTimeMs,
durationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) {
assetIdentifier = parseDescriptor(xpp, "AssetIdentifier");
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "Period"));
return Pair.create(
buildPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier), durationMs);
}
protected Period buildPeriod(
@Nullable String id,
long startMs,
List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams,
@Nullable Descriptor assetIdentifier) {
return new Period(id, startMs, adaptationSets, eventStreams, assetIdentifier);
}
// AdaptationSet parsing.
protected AdaptationSet parseAdaptationSet(
XmlPullParser xpp,
List<BaseUrl> parentBaseUrls,
@Nullable SegmentBase segmentBase,
long periodDurationMs,
long baseUrlAvailabilityTimeOffsetUs,
long segmentBaseAvailabilityTimeOffsetUs,
long periodStartUnixTimeMs,
long timeShiftBufferDepthMs,
boolean dvbProfileDeclared)
throws XmlPullParserException, IOException {
long id = parseLong(xpp, "id", AdaptationSet.ID_UNSET);
@C.TrackType int contentType = parseContentType(xpp);
String mimeType = xpp.getAttributeValue(null, "mimeType");
String codecs = xpp.getAttributeValue(null, "codecs");
int width = parseInt(xpp, "width", Format.NO_VALUE);
int height = parseInt(xpp, "height", Format.NO_VALUE);
float frameRate = parseFrameRate(xpp, Format.NO_VALUE);
int audioChannels = Format.NO_VALUE;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE);
String language = xpp.getAttributeValue(null, "lang");
String label = xpp.getAttributeValue(null, "label");
String drmSchemeType = null;
ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();
ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();
ArrayList<Descriptor> accessibilityDescriptors = new ArrayList<>();
ArrayList<Descriptor> roleDescriptors = new ArrayList<>();
ArrayList<Descriptor> essentialProperties = new ArrayList<>();
ArrayList<Descriptor> supplementalProperties = new ArrayList<>();
List<RepresentationInfo> representationInfos = new ArrayList<>();
ArrayList<BaseUrl> baseUrls = new ArrayList<>();
boolean seenFirstBaseUrl = false;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrlAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs);
seenFirstBaseUrl = true;
}
baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls, dvbProfileDeclared));
} else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) {
Pair<String, SchemeData> contentProtection = parseContentProtection(xpp);
if (contentProtection.first != null) {
drmSchemeType = contentProtection.first;
}
if (contentProtection.second != null) {
drmSchemeDatas.add(contentProtection.second);
}
} else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) {
language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang"));
contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));
} else if (XmlPullParserUtil.isStartTag(xpp, "Role")) {
roleDescriptors.add(parseDescriptor(xpp, "Role"));
} else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) {
accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility"));
} else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) {
essentialProperties.add(parseDescriptor(xpp, "EssentialProperty"));
} else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) {
supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty"));
} else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
RepresentationInfo representationInfo =
parseRepresentation(
xpp,
!baseUrls.isEmpty() ? baseUrls : parentBaseUrls,
mimeType,
codecs,
width,
height,
frameRate,
audioChannels,
audioSamplingRate,
language,
roleDescriptors,
accessibilityDescriptors,
essentialProperties,
supplementalProperties,
segmentBase,
periodStartUnixTimeMs,
periodDurationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs,
dvbProfileDeclared);
contentType =
checkContentTypeConsistency(
contentType, MimeTypes.getTrackType(representationInfo.format.sampleMimeType));
representationInfos.add(representationInfo);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBaseAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs);
segmentBase =
parseSegmentList(
xpp,
(SegmentList) segmentBase,
periodStartUnixTimeMs,
periodDurationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBaseAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs);
segmentBase =
parseSegmentTemplate(
xpp,
(SegmentTemplate) segmentBase,
supplementalProperties,
periodStartUnixTimeMs,
periodDurationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp, "Label")) {
label = parseLabel(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp)) {
parseAdaptationSetChild(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet"));
// Build the representations.
List<Representation> representations = new ArrayList<>(representationInfos.size());
for (int i = 0; i < representationInfos.size(); i++) {
representations.add(
buildRepresentation(
representationInfos.get(i),
label,
drmSchemeType,
drmSchemeDatas,
inbandEventStreams));
}
return buildAdaptationSet(
id,
contentType,
representations,
accessibilityDescriptors,
essentialProperties,
supplementalProperties);
}
protected AdaptationSet buildAdaptationSet(
long id,
@C.TrackType int contentType,
List<Representation> representations,
List<Descriptor> accessibilityDescriptors,
List<Descriptor> essentialProperties,
List<Descriptor> supplementalProperties) {
return new AdaptationSet(
id,
contentType,
representations,
accessibilityDescriptors,
essentialProperties,
supplementalProperties);
}
protected @C.TrackType int parseContentType(XmlPullParser xpp) {
String contentType = xpp.getAttributeValue(null, "contentType");
return TextUtils.isEmpty(contentType)
? C.TRACK_TYPE_UNKNOWN
: MimeTypes.BASE_TYPE_AUDIO.equals(contentType)
? C.TRACK_TYPE_AUDIO
: MimeTypes.BASE_TYPE_VIDEO.equals(contentType)
? C.TRACK_TYPE_VIDEO
: MimeTypes.BASE_TYPE_TEXT.equals(contentType)
? C.TRACK_TYPE_TEXT
: MimeTypes.BASE_TYPE_IMAGE.equals(contentType)
? C.TRACK_TYPE_IMAGE
: C.TRACK_TYPE_UNKNOWN;
}
/**
* Parses a ContentProtection element.
*
* @param xpp The parser from which to read.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
* @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element.
* Either or both may be null, depending on the ContentProtection element being parsed.
*/
protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection(
XmlPullParser xpp) throws XmlPullParserException, IOException {
String schemeType = null;
String licenseServerUrl = null;
byte[] data = null;
UUID uuid = null;
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
if (schemeIdUri != null) {
switch (Ascii.toLowerCase(schemeIdUri)) {
case "urn:mpeg:dash:mp4protection:2011":
schemeType = xpp.getAttributeValue(null, "value");
String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID");
if (!TextUtils.isEmpty(defaultKid)
&& !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) {
String[] defaultKidStrings = defaultKid.split("\s+");
UUID[] defaultKids = new UUID[defaultKidStrings.length];
for (int i = 0; i < defaultKidStrings.length; i++) {
defaultKids[i] = UUID.fromString(defaultKidStrings[i]);
}
data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, defaultKids, null);
uuid = C.COMMON_PSSH_UUID;
} else {
Log.w(
TAG,
"Ignoring <ContentProtection> with schemeIdUri=\"urn:mpeg:dash:mp4protection:2011\""
+ " (ClearKey) due to missing required default_KID attribute.");
}
break;
case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95":
uuid = C.PLAYREADY_UUID;
break;
case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":
uuid = C.WIDEVINE_UUID;
break;
case "urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e":
uuid = C.CLEARKEY_UUID;
break;
default:
break;
}
}
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "clearkey:Laurl") && xpp.next() == XmlPullParser.TEXT) {
licenseServerUrl = xpp.getText();
} else if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl");
} else if (data == null
&& XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh")
&& xpp.next() == XmlPullParser.TEXT) {
// The cenc:pssh element is defined in 23001-7:2015.
data = Base64.decode(xpp.getText(), Base64.DEFAULT);
uuid = PsshAtomUtil.parseUuid(data);
if (uuid == null) {
Log.w(TAG, "Skipping malformed cenc:pssh data");
data = null;
}
} else if (data == null
&& C.PLAYREADY_UUID.equals(uuid)
&& XmlPullParserUtil.isStartTag(xpp, "mspr:pro")
&& xpp.next() == XmlPullParser.TEXT) {
// The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady.
data =
PsshAtomUtil.buildPsshAtom(
C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT));
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
SchemeData schemeData =
uuid != null ? new SchemeData(uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data) : null;
return Pair.create(schemeType, schemeData);
}
/**
* Parses a child of an {@link AdaptationSet} element.
*
* <p>Called for child elements that are not specifically parsed elsewhere.
*
* @param xpp The {@link XmlPullParser} from which the child should be parsed.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
*/
protected void parseAdaptationSetChild(XmlPullParser xpp)
throws XmlPullParserException, IOException {
maybeSkipTag(xpp);
}
// Representation parsing.
protected RepresentationInfo parseRepresentation(
XmlPullParser xpp,
List<BaseUrl> parentBaseUrls,
@Nullable String adaptationSetMimeType,
@Nullable String adaptationSetCodecs,
int adaptationSetWidth,
int adaptationSetHeight,
float adaptationSetFrameRate,
int adaptationSetAudioChannels,
int adaptationSetAudioSamplingRate,
@Nullable String adaptationSetLanguage,
List<Descriptor> adaptationSetRoleDescriptors,
List<Descriptor> adaptationSetAccessibilityDescriptors,
List<Descriptor> adaptationSetEssentialProperties,
List<Descriptor> adaptationSetSupplementalProperties,
@Nullable SegmentBase segmentBase,
long periodStartUnixTimeMs,
long periodDurationMs,
long baseUrlAvailabilityTimeOffsetUs,
long segmentBaseAvailabilityTimeOffsetUs,
long timeShiftBufferDepthMs,
boolean dvbProfileDeclared)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE);
String mimeType = parseString(xpp, "mimeType", adaptationSetMimeType);
String codecs = parseString(xpp, "codecs", adaptationSetCodecs);
int width = parseInt(xpp, "width", adaptationSetWidth);
int height = parseInt(xpp, "height", adaptationSetHeight);
float frameRate = parseFrameRate(xpp, adaptationSetFrameRate);
int audioChannels = adaptationSetAudioChannels;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate);
String drmSchemeType = null;
ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();
ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();
ArrayList<Descriptor> essentialProperties = new ArrayList<>(adaptationSetEssentialProperties);
ArrayList<Descriptor> supplementalProperties =
new ArrayList<>(adaptationSetSupplementalProperties);
ArrayList<BaseUrl> baseUrls = new ArrayList<>();
boolean seenFirstBaseUrl = false;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
if (!seenFirstBaseUrl) {
baseUrlAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs);
seenFirstBaseUrl = true;
}
baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls, dvbProfileDeclared));
} else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
audioChannels = parseAudioChannelConfiguration(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBaseAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs);
segmentBase =
parseSegmentList(
xpp,
(SegmentList) segmentBase,
periodStartUnixTimeMs,
periodDurationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBaseAvailabilityTimeOffsetUs =
parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs);
segmentBase =
parseSegmentTemplate(
xpp,
(SegmentTemplate) segmentBase,
adaptationSetSupplementalProperties,
periodStartUnixTimeMs,
periodDurationMs,
baseUrlAvailabilityTimeOffsetUs,
segmentBaseAvailabilityTimeOffsetUs,
timeShiftBufferDepthMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) {
Pair<String, SchemeData> contentProtection = parseContentProtection(xpp);
if (contentProtection.first != null) {
drmSchemeType = contentProtection.first;
}
if (contentProtection.second != null) {
drmSchemeDatas.add(contentProtection.second);
}
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) {
essentialProperties.add(parseDescriptor(xpp, "EssentialProperty"));
} else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) {
supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty"));
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "Representation"));
Format format =
buildFormat(
id,
mimeType,
width,
height,
frameRate,
audioChannels,
audioSamplingRate,
bandwidth,
adaptationSetLanguage,
adaptationSetRoleDescriptors,
adaptationSetAccessibilityDescriptors,
codecs,
essentialProperties,
supplementalProperties);
segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();
return new RepresentationInfo(
format,
!baseUrls.isEmpty() ? baseUrls : parentBaseUrls,
segmentBase,
drmSchemeType,
drmSchemeDatas,
inbandEventStreams,
essentialProperties,
supplementalProperties,
Representation.REVISION_ID_DEFAULT);
}
protected Format buildFormat(
@Nullable String id,
@Nullable String containerMimeType,
int width,
int height,
float frameRate,
int audioChannels,
int audioSamplingRate,
int bitrate,
@Nullable String language,
List<Descriptor> roleDescriptors,
List<Descriptor> accessibilityDescriptors,
@Nullable String codecs,
List<Descriptor> essentialProperties,
List<Descriptor> supplementalProperties) {
@Nullable String sampleMimeType = getSampleMimeType(containerMimeType, codecs);
if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) {
sampleMimeType = parseEac3SupplementalProperties(supplementalProperties);
if (MimeTypes.AUDIO_E_AC3_JOC.equals(sampleMimeType)) {
codecs = MimeTypes.CODEC_E_AC3_JOC;
}
}
@C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors);
@C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors);
roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors);
roleFlags |= parseRoleFlagsFromProperties(essentialProperties);
roleFlags |= parseRoleFlagsFromProperties(supplementalProperties);
@Nullable Pair<Integer, Integer> tileCounts = parseTileCountFromProperties(essentialProperties);
Format.Builder formatBuilder =
new Format.Builder()
.setId(id)
.setContainerMimeType(containerMimeType)
.setSampleMimeType(sampleMimeType)
.setCodecs(codecs)
.setPeakBitrate(bitrate)
.setSelectionFlags(selectionFlags)
.setRoleFlags(roleFlags)
.setLanguage(language)
.setTileCountHorizontal(tileCounts != null ? tileCounts.first : Format.NO_VALUE)
.setTileCountVertical(tileCounts != null ? tileCounts.second : Format.NO_VALUE);
if (MimeTypes.isVideo(sampleMimeType)) {
formatBuilder.setWidth(width).setHeight(height).setFrameRate(frameRate);
} else if (MimeTypes.isAudio(sampleMimeType)) {
formatBuilder.setChannelCount(audioChannels).setSampleRate(audioSamplingRate);
} else if (MimeTypes.isText(sampleMimeType)) {
int accessibilityChannel = Format.NO_VALUE;
if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) {
accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors);
} else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) {
accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors);
}
formatBuilder.setAccessibilityChannel(accessibilityChannel);
} else if (MimeTypes.isImage(sampleMimeType)) {
formatBuilder.setWidth(width).setHeight(height);
}
return formatBuilder.build();
}
protected Representation buildRepresentation(
RepresentationInfo representationInfo,
@Nullable String label,
@Nullable String extraDrmSchemeType,
ArrayList<SchemeData> extraDrmSchemeDatas,
ArrayList<Descriptor> extraInbandEventStreams) {
Format.Builder formatBuilder = representationInfo.format.buildUpon();
if (label != null) {
formatBuilder.setLabel(label);
}
@Nullable String drmSchemeType = representationInfo.drmSchemeType;
if (drmSchemeType == null) {
drmSchemeType = extraDrmSchemeType;
}
ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;
drmSchemeDatas.addAll(extraDrmSchemeDatas);
if (!drmSchemeDatas.isEmpty()) {
fillInClearKeyInformation(drmSchemeDatas);
filterRedundantIncompleteSchemeDatas(drmSchemeDatas);
formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas));
}
ArrayList<Descriptor> inbandEventStreams = representationInfo.inbandEventStreams;
inbandEventStreams.addAll(extraInbandEventStreams);
return Representation.newInstance(
representationInfo.revisionId,
formatBuilder.build(),
representationInfo.baseUrls,
representationInfo.segmentBase,
inbandEventStreams,
representationInfo.essentialProperties,
representationInfo.supplementalProperties,
/* cacheKey= */ null);
}
// SegmentBase, SegmentList and SegmentTemplate parsing.
protected SingleSegmentBase parseSegmentBase(
XmlPullParser xpp, @Nullable 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 : 0;
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;
}
@Nullable RangedUri initialization = parent != null ? parent.initialization : null;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp);
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "SegmentBase"));
return buildSingleSegmentBase(
initialization, timescale, presentationTimeOffset, indexStart, indexLength);
}
protected SingleSegmentBase buildSingleSegmentBase(
RangedUri initialization,
long timescale,
long presentationTimeOffset,
long indexStart,
long indexLength) {
return new SingleSegmentBase(
initialization, timescale, presentationTimeOffset, indexStart, indexLength);
}
protected SegmentList parseSegmentList(
XmlPullParser xpp,
@Nullable SegmentList parent,
long periodStartUnixTimeMs,
long periodDurationMs,
long baseUrlAvailabilityTimeOffsetUs,
long segmentBaseAvailabilityTimeOffsetUs,
long timeShiftBufferDepthMs)
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 : C.TIME_UNSET);
long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1);
long availabilityTimeOffsetUs =
getFinalAvailabilityTimeOffset(
baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs);
RangedUri initialization = null;
List<SegmentTimelineElement> timeline = null;
List<RangedUri> segments = null;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) {
if (segments == null) {
segments = new ArrayList<>();
}
segments.add(parseSegmentUrl(xpp));
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "SegmentList"));
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
timeline = timeline != null ? timeline : parent.segmentTimeline;
segments = segments != null ? segments : parent.mediaSegments;
}
return buildSegmentList(
initialization,
timescale,
presentationTimeOffset,
startNumber,
duration,
timeline,
availabilityTimeOffsetUs,
segments,
timeShiftBufferDepthMs,
periodStartUnixTimeMs);
}
protected SegmentList buildSegmentList(
RangedUri initialization,
long timescale,
long presentationTimeOffset,
long startNumber,
long duration,
@Nullable List<SegmentTimelineElement> timeline,
long availabilityTimeOffsetUs,
@Nullable List<RangedUri> segments,
long timeShiftBufferDepthMs,
long periodStartUnixTimeMs) {
return new SegmentList(
initialization,
timescale,
presentationTimeOffset,
startNumber,
duration,
timeline,
availabilityTimeOffsetUs,
segments,
Util.msToUs(timeShiftBufferDepthMs),
Util.msToUs(periodStartUnixTimeMs));
}
protected SegmentTemplate parseSegmentTemplate(
XmlPullParser xpp,
@Nullable SegmentTemplate parent,
List<Descriptor> adaptationSetSupplementalProperties,
long periodStartUnixTimeMs,
long periodDurationMs,
long baseUrlAvailabilityTimeOffsetUs,
long segmentBaseAvailabilityTimeOffsetUs,
long timeShiftBufferDepthMs)
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 : C.TIME_UNSET);
long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1);
long endNumber =
parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties);
long availabilityTimeOffsetUs =
getFinalAvailabilityTimeOffset(
baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs);
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 (XmlPullParserUtil.isStartTag(xpp, "Initialization")) {
initialization = parseInitialization(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) {
timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs);
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTemplate"));
if (parent != null) {
initialization = initialization != null ? initialization : parent.initialization;
timeline = timeline != null ? timeline : parent.segmentTimeline;
}
return buildSegmentTemplate(
initialization,
timescale,
presentationTimeOffset,
startNumber,
endNumber,
duration,
timeline,
availabilityTimeOffsetUs,
initializationTemplate,
mediaTemplate,
timeShiftBufferDepthMs,
periodStartUnixTimeMs);
}
protected SegmentTemplate buildSegmentTemplate(
RangedUri initialization,
long timescale,
long presentationTimeOffset,
long startNumber,
long endNumber,
long duration,
List<SegmentTimelineElement> timeline,
long availabilityTimeOffsetUs,
@Nullable UrlTemplate initializationTemplate,
@Nullable UrlTemplate mediaTemplate,
long timeShiftBufferDepthMs,
long periodStartUnixTimeMs) {
return new SegmentTemplate(
initialization,
timescale,
presentationTimeOffset,
startNumber,
endNumber,
duration,
timeline,
availabilityTimeOffsetUs,
initializationTemplate,
mediaTemplate,
Util.msToUs(timeShiftBufferDepthMs),
Util.msToUs(periodStartUnixTimeMs));
}
/**
* Parses a single EventStream node in the manifest.
*
* @param xpp The current xml parser.
* @return The {@link EventStream} parsed from this EventStream node.
* @throws XmlPullParserException If there is any error parsing this node.
* @throws IOException If there is any error reading from the underlying input stream.
*/
protected EventStream parseEventStream(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeIdUri = parseString(xpp, "schemeIdUri", "");
String value = parseString(xpp, "value", "");
long timescale = parseLong(xpp, "timescale", 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", 0);
List<Pair<Long, EventMessage>> eventMessages = new ArrayList<>();
ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512);
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Event")) {
Pair<Long, EventMessage> event =
parseEvent(
xpp, schemeIdUri, value, timescale, presentationTimeOffset, scratchOutputStream);
eventMessages.add(event);
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "EventStream"));
long[] presentationTimesUs = new long[eventMessages.size()];
EventMessage[] events = new EventMessage[eventMessages.size()];
for (int i = 0; i < eventMessages.size(); i++) {
Pair<Long, EventMessage> event = eventMessages.get(i);
presentationTimesUs[i] = event.first;
events[i] = event.second;
}
return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events);
}
protected EventStream buildEventStream(
String schemeIdUri,
String value,
long timescale,
long[] presentationTimesUs,
EventMessage[] events) {
return new EventStream(schemeIdUri, value, timescale, presentationTimesUs, events);
}
/**
* Parses a single Event node in the manifest.
*
* @param xpp The current xml parser.
* @param schemeIdUri The schemeIdUri of the parent EventStream.
* @param value The schemeIdUri of the parent EventStream.
* @param timescale The timescale of the parent EventStream.
* @param presentationTimeOffset The unscaled presentation time offset of the parent EventStream.
* @param scratchOutputStream A {@link ByteArrayOutputStream} that is used when parsing event
* objects.
* @return A pair containing the node's presentation timestamp in microseconds and the parsed
* {@link EventMessage}.
* @throws XmlPullParserException If there is any error parsing this node.
* @throws IOException If there is any error reading from the underlying input stream.
*/
protected Pair<Long, EventMessage> parseEvent(
XmlPullParser xpp,
String schemeIdUri,
String value,
long timescale,
long presentationTimeOffset,
ByteArrayOutputStream scratchOutputStream)
throws IOException, XmlPullParserException {
long id = parseLong(xpp, "id", 0);
long duration = parseLong(xpp, "duration", C.TIME_UNSET);
long presentationTime = parseLong(xpp, "presentationTime", 0);
long durationMs = Util.scaleLargeTimestamp(duration, C.MILLIS_PER_SECOND, timescale);
long presentationTimesUs =
Util.scaleLargeTimestamp(
presentationTime - presentationTimeOffset, C.MICROS_PER_SECOND, timescale);
String messageData = parseString(xpp, "messageData", null);
byte[] eventObject = parseEventObject(xpp, scratchOutputStream);
return Pair.create(
presentationTimesUs,
buildEvent(
schemeIdUri,
value,
id,
durationMs,
messageData == null ? eventObject : Util.getUtf8Bytes(messageData)));
}
/**
* Parses an event object.
*
* @param xpp The current xml parser.
* @param scratchOutputStream A {@link ByteArrayOutputStream} that's used when parsing the object.
* @return The serialized byte array.
* @throws XmlPullParserException If there is any error parsing this node.
* @throws IOException If there is any error reading from the underlying input stream.
*/
protected byte[] parseEventObject(XmlPullParser xpp, ByteArrayOutputStream scratchOutputStream)
throws XmlPullParserException, IOException {
scratchOutputStream.reset();
XmlSerializer xmlSerializer = Xml.newSerializer();
xmlSerializer.setOutput(scratchOutputStream, Charsets.UTF_8.name());
// Start reading everything between <Event> and </Event>, and serialize them into an Xml
// byte array.
xpp.nextToken();
while (!XmlPullParserUtil.isEndTag(xpp, "Event")) {
switch (xpp.getEventType()) {
case XmlPullParser.START_DOCUMENT:
xmlSerializer.startDocument(null, false);
break;
case XmlPullParser.END_DOCUMENT:
xmlSerializer.endDocument();
break;
case XmlPullParser.START_TAG:
xmlSerializer.startTag(xpp.getNamespace(), xpp.getName());
for (int i = 0; i < xpp.getAttributeCount(); i++) {
xmlSerializer.attribute(
xpp.getAttributeNamespace(i), xpp.getAttributeName(i), xpp.getAttributeValue(i));
}
break;
case XmlPullParser.END_TAG:
xmlSerializer.endTag(xpp.getNamespace(), xpp.getName());
break;
case XmlPullParser.TEXT:
xmlSerializer.text(xpp.getText());
break;
case XmlPullParser.CDSECT:
xmlSerializer.cdsect(xpp.getText());
break;
case XmlPullParser.ENTITY_REF:
xmlSerializer.entityRef(xpp.getText());
break;
case XmlPullParser.IGNORABLE_WHITESPACE:
xmlSerializer.ignorableWhitespace(xpp.getText());
break;
case XmlPullParser.PROCESSING_INSTRUCTION:
xmlSerializer.processingInstruction(xpp.getText());
break;
case XmlPullParser.COMMENT:
xmlSerializer.comment(xpp.getText());
break;
case XmlPullParser.DOCDECL:
xmlSerializer.docdecl(xpp.getText());
break;
default: // fall out
}
xpp.nextToken();
}
xmlSerializer.flush();
return scratchOutputStream.toByteArray();
}
protected EventMessage buildEvent(
String schemeIdUri, String value, long id, long durationMs, byte[] messageData) {
return new EventMessage(schemeIdUri, value, durationMs, id, messageData);
}
protected List<SegmentTimelineElement> parseSegmentTimeline(
XmlPullParser xpp, long timescale, long periodDurationMs)
throws XmlPullParserException, IOException {
List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();
long startTime = 0;
long elementDuration = C.TIME_UNSET;
int elementRepeatCount = 0;
boolean havePreviousTimelineElement = false;
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "S")) {
long newStartTime = parseLong(xpp, "t", C.TIME_UNSET);
if (havePreviousTimelineElement) {
startTime =
addSegmentTimelineElementsToList(
segmentTimeline,
startTime,
elementDuration,
elementRepeatCount,
/* endTime= */ newStartTime);
}
if (newStartTime != C.TIME_UNSET) {
startTime = newStartTime;
}
elementDuration = parseLong(xpp, "d", C.TIME_UNSET);
elementRepeatCount = parseInt(xpp, "r", 0);
havePreviousTimelineElement = true;
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTimeline"));
if (havePreviousTimelineElement) {
long periodDuration = Util.scaleLargeTimestamp(periodDurationMs, timescale, 1000);
addSegmentTimelineElementsToList(
segmentTimeline,
startTime,
elementDuration,
elementRepeatCount,
/* endTime= */ periodDuration);
}
return segmentTimeline;
}
/**
* Adds timeline elements for one S tag to the segment timeline.
*
* @param startTime Start time of the first timeline element.
* @param elementDuration Duration of one timeline element.
* @param elementRepeatCount Number of timeline elements minus one. May be negative to indicate
* that the count is determined by the total duration and the element duration.
* @param endTime End time of the last timeline element for this S tag, or {@link C#TIME_UNSET} if
* unknown. Only needed if {@code repeatCount} is negative.
* @return Calculated next start time.
*/
private long addSegmentTimelineElementsToList(
List<SegmentTimelineElement> segmentTimeline,
long startTime,
long elementDuration,
int elementRepeatCount,
long endTime) {
int count =
elementRepeatCount >= 0
? 1 + elementRepeatCount
: (int) Util.ceilDivide(endTime - startTime, elementDuration);
for (int i = 0; i < count; i++) {
segmentTimeline.add(buildSegmentTimelineElement(startTime, elementDuration));
startTime += elementDuration;
}
return startTime;
}
protected SegmentTimelineElement buildSegmentTimelineElement(long startTime, long duration) {
return new SegmentTimelineElement(startTime, duration);
}
@Nullable
protected UrlTemplate parseUrlTemplate(
XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) {
String valueString = xpp.getAttributeValue(null, name);
if (valueString != null) {
return UrlTemplate.compile(valueString);
}
return defaultValue;
}
protected RangedUri parseInitialization(XmlPullParser xpp) {
return parseRangedUrl(xpp, "sourceURL", "range");
}
protected RangedUri parseSegmentUrl(XmlPullParser xpp) {
return parseRangedUrl(xpp, "media", "mediaRange");
}
protected RangedUri parseRangedUrl(
XmlPullParser xpp, String urlAttribute, String rangeAttribute) {
String urlText = xpp.getAttributeValue(null, urlAttribute);
long rangeStart = 0;
long rangeLength = C.LENGTH_UNSET;
String rangeText = xpp.getAttributeValue(null, rangeAttribute);
if (rangeText != null) {
String[] rangeTextArray = rangeText.split("-");
rangeStart = Long.parseLong(rangeTextArray[0]);
if (rangeTextArray.length == 2) {
rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;
}
}
return buildRangedUri(urlText, rangeStart, rangeLength);
}
protected RangedUri buildRangedUri(String urlText, long rangeStart, long rangeLength) {
return new RangedUri(urlText, rangeStart, rangeLength);
}
protected ProgramInformation parseProgramInformation(XmlPullParser xpp)
throws IOException, XmlPullParserException {
String title = null;
String source = null;
String copyright = null;
String moreInformationURL = parseString(xpp, "moreInformationURL", null);
String lang = parseString(xpp, "lang", null);
do {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Title")) {
title = xpp.nextText();
} else if (XmlPullParserUtil.isStartTag(xpp, "Source")) {
source = xpp.nextText();
} else if (XmlPullParserUtil.isStartTag(xpp, "Copyright")) {
copyright = xpp.nextText();
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ProgramInformation"));
return new ProgramInformation(title, source, copyright, moreInformationURL, lang);
}
/**
* Parses a Label element.
*
* @param xpp The parser from which to read.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
* @return The parsed label.
*/
protected String parseLabel(XmlPullParser xpp) throws XmlPullParserException, IOException {
return parseText(xpp, "Label");
}
/**
* Parses a BaseURL element.
*
* @param xpp The parser from which to read.
* @param parentBaseUrls The parent base URLs for resolving the parsed URLs.
* @param dvbProfileDeclared Whether the dvb profile is declared.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
* @return The list of parsed and resolved URLs.
*/
protected List<BaseUrl> parseBaseUrl(
XmlPullParser xpp, List<BaseUrl> parentBaseUrls, boolean dvbProfileDeclared)
throws XmlPullParserException, IOException {
@Nullable String priorityValue = xpp.getAttributeValue(null, "dvb:priority");
int priority =
priorityValue != null
? Integer.parseInt(priorityValue)
: (dvbProfileDeclared ? DEFAULT_DVB_PRIORITY : PRIORITY_UNSET);
@Nullable String weightValue = xpp.getAttributeValue(null, "dvb:weight");
int weight = weightValue != null ? Integer.parseInt(weightValue) : DEFAULT_WEIGHT;
@Nullable String serviceLocation = xpp.getAttributeValue(null, "serviceLocation");
String baseUrl = parseText(xpp, "BaseURL");
if (UriUtil.isAbsolute(baseUrl)) {
if (serviceLocation == null) {
serviceLocation = baseUrl;
}
return Lists.newArrayList(new BaseUrl(baseUrl, serviceLocation, priority, weight));
}
List<BaseUrl> baseUrls = new ArrayList<>();
for (int i = 0; i < parentBaseUrls.size(); i++) {
BaseUrl parentBaseUrl = parentBaseUrls.get(i);
String resolvedBaseUri = UriUtil.resolve(parentBaseUrl.url, baseUrl);
String resolvedServiceLocation = serviceLocation == null ? resolvedBaseUri : serviceLocation;
if (dvbProfileDeclared) {
// Inherit parent properties only if dvb profile is declared.
priority = parentBaseUrl.priority;
weight = parentBaseUrl.weight;
resolvedServiceLocation = parentBaseUrl.serviceLocation;
}
baseUrls.add(new BaseUrl(resolvedBaseUri, resolvedServiceLocation, priority, weight));
}
return baseUrls;
}
/**
* Parses the availabilityTimeOffset value and returns the parsed value or the parent value if it
* doesn't exist.
*
* @param xpp The parser from which to read.
* @param parentAvailabilityTimeOffsetUs The availability time offset of a parent element in
* microseconds.
* @return The parsed availabilityTimeOffset in microseconds.
*/
protected long parseAvailabilityTimeOffsetUs(
XmlPullParser xpp, long parentAvailabilityTimeOffsetUs) {
String value = xpp.getAttributeValue(/* namespace= */ null, "availabilityTimeOffset");
if (value == null) {
return parentAvailabilityTimeOffsetUs;
}
if ("INF".equals(value)) {
return Long.MAX_VALUE;
}
return (long) (Float.parseFloat(value) * C.MICROS_PER_SECOND);
}
// AudioChannelConfiguration parsing.
protected int parseAudioChannelConfiguration(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeIdUri = parseString(xpp, "schemeIdUri", null);
int audioChannels;
switch (schemeIdUri) {
case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011":
audioChannels = parseInt(xpp, "value", Format.NO_VALUE);
break;
case "urn:mpeg:mpegB:cicp:ChannelConfiguration":
audioChannels = parseMpegChannelConfiguration(xpp);
break;
case "tag:dts.com,2014:dash:audio_channel_configuration:2012":
case "urn:dts:dash:audio_channel_configuration:2012":
audioChannels = parseDtsChannelConfiguration(xpp);
break;
case "tag:dts.com,2018:uhd:audio_channel_configuration":
audioChannels = parseDtsxChannelConfiguration(xpp);
break;
case "tag:dolby.com,2014:dash:audio_channel_configuration:2011":
case "urn:dolby:dash:audio_channel_configuration:2011":
audioChannels = parseDolbyChannelConfiguration(xpp);
break;
default:
audioChannels = Format.NO_VALUE;
break;
}
do {
xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
return audioChannels;
}
// Selection flag parsing.
protected @C.SelectionFlags int parseSelectionFlagsFromRoleDescriptors(
List<Descriptor> roleDescriptors) {
@C.SelectionFlags int result = 0;
for (int i = 0; i < roleDescriptors.size(); i++) {
Descriptor descriptor = roleDescriptors.get(i);
if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
result |= parseSelectionFlagsFromDashRoleScheme(descriptor.value);
}
}
return result;
}
protected @C.SelectionFlags int parseSelectionFlagsFromDashRoleScheme(@Nullable String value) {
if (value == null) {
return 0;
}
switch (value) {
case "forced_subtitle":
// Support both hyphen and underscore (https://github.com/google/ExoPlayer/issues/9727).
case "forced-subtitle":
return C.SELECTION_FLAG_FORCED;
default:
return 0;
}
}
// Role and Accessibility parsing.
protected @C.RoleFlags int parseRoleFlagsFromRoleDescriptors(List<Descriptor> roleDescriptors) {
@C.RoleFlags int result = 0;
for (int i = 0; i < roleDescriptors.size(); i++) {
Descriptor descriptor = roleDescriptors.get(i);
if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
result |= parseRoleFlagsFromDashRoleScheme(descriptor.value);
}
}
return result;
}
protected @C.RoleFlags int parseRoleFlagsFromAccessibilityDescriptors(
List<Descriptor> accessibilityDescriptors) {
@C.RoleFlags int result = 0;
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
Descriptor descriptor = accessibilityDescriptors.get(i);
if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
result |= parseRoleFlagsFromDashRoleScheme(descriptor.value);
} else if (Ascii.equalsIgnoreCase(
"urn:tva:metadata:cs:AudioPurposeCS:2007", descriptor.schemeIdUri)) {
result |= parseTvaAudioPurposeCsValue(descriptor.value);
}
}
return result;
}
protected @C.RoleFlags int parseRoleFlagsFromProperties(
List<Descriptor> accessibilityDescriptors) {
@C.RoleFlags int result = 0;
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
Descriptor descriptor = accessibilityDescriptors.get(i);
if (Ascii.equalsIgnoreCase(
"http://dashif.org/guidelines/trickmode", descriptor.schemeIdUri)) {
result |= C.ROLE_FLAG_TRICK_PLAY;
}
}
return result;
}
protected @C.RoleFlags int parseRoleFlagsFromDashRoleScheme(@Nullable String value) {
if (value == null) {
return 0;
}
switch (value) {
case "main":
return C.ROLE_FLAG_MAIN;
case "alternate":
return C.ROLE_FLAG_ALTERNATE;
case "supplementary":
return C.ROLE_FLAG_SUPPLEMENTARY;
case "commentary":
return C.ROLE_FLAG_COMMENTARY;
case "dub":
return C.ROLE_FLAG_DUB;
case "emergency":
return C.ROLE_FLAG_EMERGENCY;
case "caption":
return C.ROLE_FLAG_CAPTION;
case "forced_subtitle":
// Support both hyphen and underscore (https://github.com/google/ExoPlayer/issues/9727).
case "forced-subtitle":
case "subtitle":
return C.ROLE_FLAG_SUBTITLE;
case "sign":
return C.ROLE_FLAG_SIGN;
case "description":
return C.ROLE_FLAG_DESCRIBES_VIDEO;
case "enhanced-audio-intelligibility":
return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY;
default:
return 0;
}
}
protected @C.RoleFlags int parseTvaAudioPurposeCsValue(@Nullable String value) {
if (value == null) {
return 0;
}
switch (value) {
case "1": // Audio description for the visually impaired.
return C.ROLE_FLAG_DESCRIBES_VIDEO;
case "2": // Audio description for the hard of hearing.
return C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY;
case "3": // Supplemental commentary.
return C.ROLE_FLAG_SUPPLEMENTARY;
case "4": // Director's commentary.
return C.ROLE_FLAG_COMMENTARY;
case "6": // Main programme audio.
return C.ROLE_FLAG_MAIN;
default:
return 0;
}
}
protected String[] parseProfiles(XmlPullParser xpp, String attributeName, String[] defaultValue) {
@Nullable String attributeValue = xpp.getAttributeValue(/* namespace= */ null, attributeName);
if (attributeValue == null) {
return defaultValue;
}
return attributeValue.split(",");
}
// Thumbnail tile information parsing
/**
* Parses given descriptors for thumbnail tile information.
*
* @param essentialProperties List of descriptors that contain thumbnail tile information.
* @return A pair of Integer values, where the first is the count of horizontal tiles and the
* second is the count of vertical tiles, or null if no thumbnail tile information is found.
*/
@Nullable
protected Pair<Integer, Integer> parseTileCountFromProperties(
List<Descriptor> essentialProperties) {
for (int i = 0; i < essentialProperties.size(); i++) {
Descriptor descriptor = essentialProperties.get(i);
if ((Ascii.equalsIgnoreCase("http://dashif.org/thumbnail_tile", descriptor.schemeIdUri)
|| Ascii.equalsIgnoreCase(
"http://dashif.org/guidelines/thumbnail_tile", descriptor.schemeIdUri))
&& descriptor.value != null) {
String size = descriptor.value;
String[] sizeSplit = Util.split(size, "x");
if (sizeSplit.length != 2) {
continue;
}
try {
int tileCountHorizontal = Integer.parseInt(sizeSplit[0]);
int tileCountVertical = Integer.parseInt(sizeSplit[1]);
return Pair.create(tileCountHorizontal, tileCountVertical);
} catch (NumberFormatException e) {
// Ignore property if it's malformed.
}
}
}
return null;
}
// Utility methods.
/**
* If the provided {@link XmlPullParser} is currently positioned at the start of a tag, skips
* forward to the end of that tag.
*
* @param xpp The {@link XmlPullParser}.
* @throws XmlPullParserException If an error occurs parsing the stream.
* @throws IOException If an error occurs reading the stream.
*/
public static void maybeSkipTag(XmlPullParser xpp) throws IOException, XmlPullParserException {
if (!XmlPullParserUtil.isStartTag(xpp)) {
return;
}
int depth = 1;
while (depth != 0) {
xpp.next();
if (XmlPullParserUtil.isStartTag(xpp)) {
depth++;
} else if (XmlPullParserUtil.isEndTag(xpp)) {
depth--;
}
}
}
/** Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}. */
private static void filterRedundantIncompleteSchemeDatas(ArrayList<SchemeData> schemeDatas) {
for (int i = schemeDatas.size() - 1; i >= 0; i--) {
SchemeData schemeData = schemeDatas.get(i);
if (!schemeData.hasData()) {
for (int j = 0; j < schemeDatas.size(); j++) {
if (schemeDatas.get(j).canReplace(schemeData)) {
// schemeData is incomplete, but there is another matching SchemeData which does contain
// data, so we remove the incomplete one.
schemeDatas.remove(i);
break;
}
}
}
}
}
private static void fillInClearKeyInformation(ArrayList<SchemeData> schemeDatas) {
// Find and remove ClearKey information.
@Nullable String clearKeyLicenseServerUrl = null;
for (int i = 0; i < schemeDatas.size(); i++) {
SchemeData schemeData = schemeDatas.get(i);
if (C.CLEARKEY_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl != null) {
clearKeyLicenseServerUrl = schemeData.licenseServerUrl;
schemeDatas.remove(i);
break;
}
}
if (clearKeyLicenseServerUrl == null) {
return;
}
// Fill in the ClearKey information into the existing PSSH schema data if applicable.
for (int i = 0; i < schemeDatas.size(); i++) {
SchemeData schemeData = schemeDatas.get(i);
if (C.COMMON_PSSH_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl == null) {
schemeDatas.set(
i,
new SchemeData(
C.CLEARKEY_UUID, clearKeyLicenseServerUrl, schemeData.mimeType, schemeData.data));
}
}
}
/**
* Derives a sample mimeType from a container mimeType and codecs attribute.
*
* @param containerMimeType The mimeType of the container.
* @param codecs The codecs attribute.
* @return The derived sample mimeType, or null if it could not be derived.
*/
@Nullable
private static String getSampleMimeType(
@Nullable String containerMimeType, @Nullable String codecs) {
if (MimeTypes.isAudio(containerMimeType)) {
return MimeTypes.getAudioMediaMimeType(codecs);
} else if (MimeTypes.isVideo(containerMimeType)) {
return MimeTypes.getVideoMediaMimeType(codecs);
} else if (MimeTypes.isText(containerMimeType)) {
// Text types are raw formats.
return containerMimeType;
} else if (MimeTypes.isImage(containerMimeType)) {
// Image types are raw formats.
return containerMimeType;
} else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {
@Nullable String mimeType = MimeTypes.getMediaMimeType(codecs);
return MimeTypes.TEXT_VTT.equals(mimeType) ? MimeTypes.APPLICATION_MP4VTT : mimeType;
}
return null;
}
/**
* Checks two languages for consistency, returning the consistent language, or throwing an {@link
* IllegalStateException} if the languages are inconsistent.
*
* <p>Two languages are consistent if they are equal, or if one is null.
*
* @param firstLanguage The first language.
* @param secondLanguage The second language.
* @return The consistent language.
*/
@Nullable
private static String checkLanguageConsistency(
@Nullable String firstLanguage, @Nullable String secondLanguage) {
if (firstLanguage == null) {
return secondLanguage;
} else if (secondLanguage == null) {
return firstLanguage;
} else {
Assertions.checkState(firstLanguage.equals(secondLanguage));
return firstLanguage;
}
}
/**
* Checks two adaptation set content 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 C#TRACK_TYPE_UNKNOWN}. Where
* one of the types is {@link C#TRACK_TYPE_UNKNOWN}, the other is returned.
*
* @param firstType The first type.
* @param secondType The second type.
* @return The consistent type.
*/
private static int checkContentTypeConsistency(
@C.TrackType int firstType, @C.TrackType int secondType) {
if (firstType == C.TRACK_TYPE_UNKNOWN) {
return secondType;
} else if (secondType == C.TRACK_TYPE_UNKNOWN) {
return firstType;
} else {
Assertions.checkState(firstType == secondType);
return firstType;
}
}
/**
* Parses a {@link Descriptor} from an element.
*
* @param xpp The parser from which to read.
* @param tag The tag of the element being parsed.
* @throws XmlPullParserException If an error occurs parsing the element.
* @throws IOException If an error occurs reading the element.
* @return The parsed {@link Descriptor}.
*/
protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag)
throws XmlPullParserException, IOException {
String schemeIdUri = parseString(xpp, "schemeIdUri", "");
String value = parseString(xpp, "value", null);
String id = parseString(xpp, "id", null);
do {
xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, tag));
return new Descriptor(schemeIdUri, value, id);
}
protected static int parseCea608AccessibilityChannel(List<Descriptor> accessibilityDescriptors) {
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)
&& descriptor.value != null) {
Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
if (accessibilityValueMatcher.matches()) {
return Integer.parseInt(accessibilityValueMatcher.group(1));
} else {
Log.w(TAG, "Unable to parse CEA-608 channel number from: " + descriptor.value);
}
}
}
return Format.NO_VALUE;
}
protected static int parseCea708AccessibilityChannel(List<Descriptor> accessibilityDescriptors) {
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri)
&& descriptor.value != null) {
Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
if (accessibilityValueMatcher.matches()) {
return Integer.parseInt(accessibilityValueMatcher.group(1));
} else {
Log.w(TAG, "Unable to parse CEA-708 service block number from: " + descriptor.value);
}
}
}
return Format.NO_VALUE;
}
protected static String parseEac3SupplementalProperties(List<Descriptor> supplementalProperties) {
for (int i = 0; i < supplementalProperties.size(); i++) {
Descriptor descriptor = supplementalProperties.get(i);
String schemeIdUri = descriptor.schemeIdUri;
if (("tag:dolby.com,2018:dash:EC3_ExtensionType:2018".equals(schemeIdUri)
&& "JOC".equals(descriptor.value))
|| ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri)
&& "ec+3".equals(descriptor.value))) {
return MimeTypes.AUDIO_E_AC3_JOC;
}
}
return MimeTypes.AUDIO_E_AC3;
}
protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) {
float frameRate = defaultValue;
String frameRateAttribute = xpp.getAttributeValue(null, "frameRate");
if (frameRateAttribute != null) {
Matcher frameRateMatcher = FRAME_RATE_PATTERN.matcher(frameRateAttribute);
if (frameRateMatcher.matches()) {
int numerator = Integer.parseInt(frameRateMatcher.group(1));
String denominatorString = frameRateMatcher.group(2);
if (!TextUtils.isEmpty(denominatorString)) {
frameRate = (float) numerator / Integer.parseInt(denominatorString);
} else {
frameRate = numerator;
}
}
}
return frameRate;
}
protected 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);
}
}
protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
throws ParserException {
String value = xpp.getAttributeValue(null, name);
if (value == null) {
return defaultValue;
} else {
return Util.parseXsDateTime(value);
}
}
protected static String parseText(XmlPullParser xpp, String label)
throws XmlPullParserException, IOException {
String text = "";
do {
xpp.next();
if (xpp.getEventType() == XmlPullParser.TEXT) {
text = xpp.getText();
} else {
maybeSkipTag(xpp);
}
} while (!XmlPullParserUtil.isEndTag(xpp, label));
return text;
}
protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Integer.parseInt(value);
}
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 float parseFloat(XmlPullParser xpp, String name, float defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : Float.parseFloat(value);
}
protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {
String value = xpp.getAttributeValue(null, name);
return value == null ? defaultValue : value;
}
/**
* Parses the number of channels from the value attribute of an AudioChannelConfiguration with
* schemeIdUri "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1.
*
* @param xpp The parser from which to read.
* @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
* not be parsed.
*/
protected static int parseMpegChannelConfiguration(XmlPullParser xpp) {
int index = parseInt(xpp, "value", C.INDEX_UNSET);
return 0 <= index && index < MPEG_CHANNEL_CONFIGURATION_MAPPING.length
? MPEG_CHANNEL_CONFIGURATION_MAPPING[index]
: Format.NO_VALUE;
}
/**
* Parses the number of channels from the value attribute of an AudioChannelConfiguration with
* schemeIdUri "tag:dts.com,2014:dash:audio_channel_configuration:2012" as defined by Annex G
* (3.2) in ETSI TS 102 114 V1.6.1, or by the legacy schemeIdUri
* "urn:dts:dash:audio_channel_configuration:2012".
*
* @param xpp The parser from which to read.
* @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
* not be parsed.
*/
protected static int parseDtsChannelConfiguration(XmlPullParser xpp) {
int channelCount = parseInt(xpp, "value", Format.NO_VALUE);
return 0 < channelCount && channelCount < 33 ? channelCount : Format.NO_VALUE;
}
/**
* Parses the number of channels from the value attribute of an AudioChannelConfiguration with
* schemeIdUri "tag:dts.com,2018:uhd:audio_channel_configuration" as defined by table B-5 in ETSI
* TS 103 491 v1.2.1.
*
* @param xpp The parser from which to read.
* @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
* not be parsed.
*/
protected static int parseDtsxChannelConfiguration(XmlPullParser xpp) {
@Nullable String value = xpp.getAttributeValue(null, "value");
if (value == null) {
return Format.NO_VALUE;
}
int channelCount = Integer.bitCount(Integer.parseInt(value, /* radix= */ 16));
return channelCount == 0 ? Format.NO_VALUE : channelCount;
}
/**
* Parses the number of channels from the value attribute of an AudioChannelConfiguration with
* schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011" as defined by table E.5
* in ETSI TS 102 366, or by the legacy schemeIdUri
* "urn:dolby:dash:audio_channel_configuration:2011".
*
* @param xpp The parser from which to read.
* @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
* not be parsed.
*/
protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) {
@Nullable String value = xpp.getAttributeValue(null, "value");
if (value == null) {
return Format.NO_VALUE;
}
switch (Ascii.toLowerCase(value)) {
case "4000":
return 1;
case "a000":
return 2;
case "f800":
return 5;
case "f801":
return 6;
case "fa01":
return 8;
default:
return Format.NO_VALUE;
}
}
protected static long parseLastSegmentNumberSupplementalProperty(
List<Descriptor> supplementalProperties) {
for (int i = 0; i < supplementalProperties.size(); i++) {
Descriptor descriptor = supplementalProperties.get(i);
if (Ascii.equalsIgnoreCase(
"http://dashif.org/guidelines/last-segment-number", descriptor.schemeIdUri)) {
return Long.parseLong(descriptor.value);
}
}
return C.INDEX_UNSET;
}
private static long getFinalAvailabilityTimeOffset(
long baseUrlAvailabilityTimeOffsetUs, long segmentBaseAvailabilityTimeOffsetUs) {
long availabilityTimeOffsetUs = segmentBaseAvailabilityTimeOffsetUs;
if (availabilityTimeOffsetUs == C.TIME_UNSET) {
// Fall back to BaseURL values if no SegmentBase specifies an offset.
availabilityTimeOffsetUs = baseUrlAvailabilityTimeOffsetUs;
}
if (availabilityTimeOffsetUs == Long.MAX_VALUE) {
// Replace INF value with TIME_UNSET to specify that all segments are available immediately.
availabilityTimeOffsetUs = C.TIME_UNSET;
}
return availabilityTimeOffsetUs;
}
private boolean isDvbProfileDeclared(String[] profiles) {
for (String profile : profiles) {
if (profile.startsWith("urn:dvb:dash:profile:dvb-dash:")) {
return true;
}
}
return false;
}
/** A parsed Representation element. */
protected static final class RepresentationInfo {
public final Format format;
public final ImmutableList<BaseUrl> baseUrls;
public final SegmentBase segmentBase;
@Nullable public final String drmSchemeType;
public final ArrayList<SchemeData> drmSchemeDatas;
public final ArrayList<Descriptor> inbandEventStreams;
public final long revisionId;
public final List<Descriptor> essentialProperties;
public final List<Descriptor> supplementalProperties;
public RepresentationInfo(
Format format,
List<BaseUrl> baseUrls,
SegmentBase segmentBase,
@Nullable String drmSchemeType,
ArrayList<SchemeData> drmSchemeDatas,
ArrayList<Descriptor> inbandEventStreams,
List<Descriptor> essentialProperties,
List<Descriptor> supplementalProperties,
long revisionId) {
this.format = format;
this.baseUrls = ImmutableList.copyOf(baseUrls);
this.segmentBase = segmentBase;
this.drmSchemeType = drmSchemeType;
this.drmSchemeDatas = drmSchemeDatas;
this.inbandEventStreams = inbandEventStreams;
this.essentialProperties = essentialProperties;
this.supplementalProperties = supplementalProperties;
this.revisionId = revisionId;
}
}
}