/*
* 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.smoothstreaming.manifest;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Pair;
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.CodecSpecificDataUtil;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest.ProtectionElement;
import androidx.media3.exoplayer.smoothstreaming.manifest.SsManifest.StreamElement;
import androidx.media3.exoplayer.upstream.ParsingLoadable;
import androidx.media3.extractor.AacUtil;
import androidx.media3.extractor.mp4.PsshAtomUtil;
import androidx.media3.extractor.mp4.TrackEncryptionBox;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
/**
* Parses SmoothStreaming client manifests.
*
* <p>See the <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">IIS Smooth
* Streaming Client Manifest Format</a>
*/
@UnstableApi
public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
private final XmlPullParserFactory xmlParserFactory;
public SsManifestParser() {
try {
xmlParserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
}
}
@Override
public SsManifest parse(Uri uri, InputStream inputStream) throws IOException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, null);
SmoothStreamingMediaParser smoothStreamingMediaParser =
new SmoothStreamingMediaParser(null, uri.toString());
return (SsManifest) smoothStreamingMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, /* cause= */ e);
}
}
/** Thrown if a required field is missing. */
public static class MissingFieldException extends ParserException {
public MissingFieldException(String fieldName) {
super(
"Missing required field: " + fieldName,
/* cause= */ null,
/* contentIsMalformed= */ true,
C.DATA_TYPE_MANIFEST);
}
}
/** A base class for parsers that parse components of a smooth streaming manifest. */
private abstract static class ElementParser {
private final String baseUri;
private final String tag;
@Nullable private final ElementParser parent;
private final List<Pair<String, @NullableType Object>> normalizedAttributes;
public ElementParser(@Nullable ElementParser parent, String baseUri, String tag) {
this.parent = parent;
this.baseUri = baseUri;
this.tag = tag;
this.normalizedAttributes = new LinkedList<>();
}
public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException {
String tagName;
boolean foundStartTag = false;
int skippingElementDepth = 0;
while (true) {
int eventType = xmlParser.getEventType();
switch (eventType) {
case XmlPullParser.START_TAG:
tagName = xmlParser.getName();
if (tag.equals(tagName)) {
foundStartTag = true;
parseStartTag(xmlParser);
} else if (foundStartTag) {
if (skippingElementDepth > 0) {
skippingElementDepth++;
} else if (handleChildInline(tagName)) {
parseStartTag(xmlParser);
} else {
ElementParser childElementParser = newChildParser(this, tagName, baseUri);
if (childElementParser == null) {
skippingElementDepth = 1;
} else {
addChild(childElementParser.parse(xmlParser));
}
}
}
break;
case XmlPullParser.TEXT:
if (foundStartTag && skippingElementDepth == 0) {
parseText(xmlParser);
}
break;
case XmlPullParser.END_TAG:
if (foundStartTag) {
if (skippingElementDepth > 0) {
skippingElementDepth--;
} else {
tagName = xmlParser.getName();
parseEndTag(xmlParser);
if (!handleChildInline(tagName)) {
return build();
}
}
}
break;
case XmlPullParser.END_DOCUMENT:
return null;
default:
// Do nothing.
break;
}
xmlParser.next();
}
}
private ElementParser newChildParser(ElementParser parent, String name, String baseUri) {
if (QualityLevelParser.TAG.equals(name)) {
return new QualityLevelParser(parent, baseUri);
} else if (ProtectionParser.TAG.equals(name)) {
return new ProtectionParser(parent, baseUri);
} else if (StreamIndexParser.TAG.equals(name)) {
return new StreamIndexParser(parent, baseUri);
}
return null;
}
/**
* Stash an attribute that may be normalized at this level. In other words, an attribute that
* may have been pulled up from the child elements because its value was the same in all
* children.
*
* <p>Stashing an attribute allows child element parsers to retrieve the values of normalized
* attributes using {@link #getNormalizedAttribute(String)}.
*
* @param key The name of the attribute.
* @param value The value of the attribute.
*/
protected final void putNormalizedAttribute(String key, @Nullable Object value) {
normalizedAttributes.add(Pair.create(key, value));
}
/**
* Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with the
* provided name, the parent element parser will be queried, and so on up the chain.
*
* @param key The name of the attribute.
* @return The stashed value, or null if the attribute was not found.
*/
@Nullable
protected final Object getNormalizedAttribute(String key) {
for (int i = 0; i < normalizedAttributes.size(); i++) {
Pair<String, Object> pair = normalizedAttributes.get(i);
if (pair.first.equals(key)) {
return pair.second;
}
}
return parent == null ? null : parent.getNormalizedAttribute(key);
}
/**
* Whether this {@link ElementParser} parses a child element inline.
*
* @param tagName The name of the child element.
* @return Whether the child is parsed inline.
*/
protected boolean handleChildInline(String tagName) {
return false;
}
/**
* @param xmlParser The underlying {@link XmlPullParser}
* @throws ParserException If a parsing error occurs.
*/
protected void parseStartTag(XmlPullParser xmlParser) throws ParserException {
// Do nothing.
}
/**
* @param xmlParser The underlying {@link XmlPullParser}
*/
protected void parseText(XmlPullParser xmlParser) {
// Do nothing.
}
/**
* @param xmlParser The underlying {@link XmlPullParser}
*/
protected void parseEndTag(XmlPullParser xmlParser) {
// Do nothing.
}
/**
* @param parsedChild A parsed child object.
*/
protected void addChild(Object parsedChild) {
// Do nothing.
}
protected abstract Object build();
protected final String parseRequiredString(XmlPullParser parser, String key)
throws MissingFieldException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
return value;
} else {
throw new MissingFieldException(key);
}
}
protected final int parseInt(XmlPullParser parser, String key, int defaultValue)
throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, /* cause= */ e);
}
} else {
return defaultValue;
}
}
protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, /* cause= */ e);
}
} else {
throw new MissingFieldException(key);
}
}
protected final long parseLong(XmlPullParser parser, String key, long defaultValue)
throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, /* cause= */ e);
}
} else {
return defaultValue;
}
}
protected final long parseRequiredLong(XmlPullParser parser, String key)
throws ParserException {
String value = parser.getAttributeValue(null, key);
if (value != null) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, /* cause= */ e);
}
} else {
throw new MissingFieldException(key);
}
}
protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) {
String value = parser.getAttributeValue(null, key);
if (value != null) {
return Boolean.parseBoolean(value);
} else {
return defaultValue;
}
}
}
private static class SmoothStreamingMediaParser extends ElementParser {
public static final String TAG = "SmoothStreamingMedia";
private static final String KEY_MAJOR_VERSION = "MajorVersion";
private static final String KEY_MINOR_VERSION = "MinorVersion";
private static final String KEY_TIME_SCALE = "TimeScale";
private static final String KEY_DVR_WINDOW_LENGTH = "DVRWindowLength";
private static final String KEY_DURATION = "Duration";
private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
private static final String KEY_IS_LIVE = "IsLive";
private final List<StreamElement> streamElements;
private int majorVersion;
private int minorVersion;
private long timescale;
private long duration;
private long dvrWindowLength;
private int lookAheadCount;
private boolean isLive;
@Nullable private ProtectionElement protectionElement;
public SmoothStreamingMediaParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
lookAheadCount = SsManifest.UNSET_LOOKAHEAD;
protectionElement = null;
streamElements = new LinkedList<>();
}
@Override
public void parseStartTag(XmlPullParser parser) throws ParserException {
majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);
minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);
timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L);
duration = parseRequiredLong(parser, KEY_DURATION);
dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0);
lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, SsManifest.UNSET_LOOKAHEAD);
isLive = parseBoolean(parser, KEY_IS_LIVE, false);
putNormalizedAttribute(KEY_TIME_SCALE, timescale);
}
@Override
public void addChild(Object child) {
if (child instanceof StreamElement) {
streamElements.add((StreamElement) child);
} else if (child instanceof ProtectionElement) {
Assertions.checkState(protectionElement == null);
protectionElement = (ProtectionElement) child;
}
}
@Override
public Object build() {
StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
streamElements.toArray(streamElementArray);
if (protectionElement != null) {
DrmInitData drmInitData =
new DrmInitData(
new SchemeData(
protectionElement.uuid, MimeTypes.VIDEO_MP4, protectionElement.data));
for (StreamElement streamElement : streamElementArray) {
int type = streamElement.type;
if (type == C.TRACK_TYPE_VIDEO || type == C.TRACK_TYPE_AUDIO) {
Format[] formats = streamElement.formats;
for (int i = 0; i < formats.length; i++) {
formats[i] = formats[i].buildUpon().setDrmInitData(drmInitData).build();
}
}
}
}
return new SsManifest(
majorVersion,
minorVersion,
timescale,
duration,
dvrWindowLength,
lookAheadCount,
isLive,
protectionElement,
streamElementArray);
}
}
private static class ProtectionParser extends ElementParser {
public static final String TAG = "Protection";
public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";
public static final String KEY_SYSTEM_ID = "SystemID";
private static final int INITIALIZATION_VECTOR_SIZE = 8;
private boolean inProtectionHeader;
private UUID uuid;
private byte[] initData;
public ProtectionParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
}
@Override
public boolean handleChildInline(String tag) {
return TAG_PROTECTION_HEADER.equals(tag);
}
@Override
public void parseStartTag(XmlPullParser parser) {
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
inProtectionHeader = true;
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
uuidString = stripCurlyBraces(uuidString);
uuid = UUID.fromString(uuidString);
}
}
@Override
public void parseText(XmlPullParser parser) {
if (inProtectionHeader) {
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
}
}
@Override
public void parseEndTag(XmlPullParser parser) {
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
inProtectionHeader = false;
}
}
@Override
public Object build() {
return new ProtectionElement(
uuid, PsshAtomUtil.buildPsshAtom(uuid, initData), buildTrackEncryptionBoxes(initData));
}
private static TrackEncryptionBox[] buildTrackEncryptionBoxes(byte[] initData) {
return new TrackEncryptionBox[] {
new TrackEncryptionBox(
/* isEncrypted= */ true,
/* schemeType= */ null,
INITIALIZATION_VECTOR_SIZE,
getProtectionElementKeyId(initData),
/* defaultEncryptedBlocks= */ 0,
/* defaultClearBlocks= */ 0,
/* defaultInitializationVector= */ null)
};
}
private static byte[] getProtectionElementKeyId(byte[] initData) {
StringBuilder initDataStringBuilder = new StringBuilder();
for (int i = 0; i < initData.length; i += 2) {
initDataStringBuilder.append((char) initData[i]);
}
String initDataString = initDataStringBuilder.toString();
String keyIdString =
initDataString.substring(
initDataString.indexOf("<KID>") + 5, initDataString.indexOf("</KID>"));
byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT);
swap(keyId, 0, 3);
swap(keyId, 1, 2);
swap(keyId, 4, 5);
swap(keyId, 6, 7);
return keyId;
}
private static void swap(byte[] data, int firstPosition, int secondPosition) {
byte temp = data[firstPosition];
data[firstPosition] = data[secondPosition];
data[secondPosition] = temp;
}
private static String stripCurlyBraces(String uuidString) {
if (uuidString.charAt(0) == '{' && uuidString.charAt(uuidString.length() - 1) == '}') {
uuidString = uuidString.substring(1, uuidString.length() - 1);
}
return uuidString;
}
}
private static class StreamIndexParser extends ElementParser {
public static final String TAG = "StreamIndex";
private static final String TAG_STREAM_FRAGMENT = "c";
private static final String KEY_TYPE = "Type";
private static final String KEY_TYPE_AUDIO = "audio";
private static final String KEY_TYPE_VIDEO = "video";
private static final String KEY_TYPE_TEXT = "text";
private static final String KEY_SUB_TYPE = "Subtype";
private static final String KEY_NAME = "Name";
private static final String KEY_URL = "Url";
private static final String KEY_MAX_WIDTH = "MaxWidth";
private static final String KEY_MAX_HEIGHT = "MaxHeight";
private static final String KEY_DISPLAY_WIDTH = "DisplayWidth";
private static final String KEY_DISPLAY_HEIGHT = "DisplayHeight";
private static final String KEY_LANGUAGE = "Language";
private static final String KEY_TIME_SCALE = "TimeScale";
private static final String KEY_FRAGMENT_DURATION = "d";
private static final String KEY_FRAGMENT_START_TIME = "t";
private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";
private final String baseUri;
private final List<Format> formats;
private int type;
private String subType;
private long timescale;
private String name;
private String url;
private int maxWidth;
private int maxHeight;
private int displayWidth;
private int displayHeight;
private String language;
private ArrayList<Long> startTimes;
private long lastChunkDuration;
public StreamIndexParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
this.baseUri = baseUri;
formats = new LinkedList<>();
}
@Override
public boolean handleChildInline(String tag) {
return TAG_STREAM_FRAGMENT.equals(tag);
}
@Override
public void parseStartTag(XmlPullParser parser) throws ParserException {
if (TAG_STREAM_FRAGMENT.equals(parser.getName())) {
parseStreamFragmentStartTag(parser);
} else {
parseStreamElementStartTag(parser);
}
}
private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
int chunkIndex = startTimes.size();
long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, C.TIME_UNSET);
if (startTime == C.TIME_UNSET) {
if (chunkIndex == 0) {
// Assume the track starts at t = 0.
startTime = 0;
} else if (lastChunkDuration != C.INDEX_UNSET) {
// Infer the start time from the previous chunk's start time and duration.
startTime = startTimes.get(chunkIndex - 1) + lastChunkDuration;
} else {
// We don't have the start time, and we're unable to infer it.
throw ParserException.createForMalformedManifest(
"Unable to infer start time", /* cause= */ null);
}
}
chunkIndex++;
startTimes.add(startTime);
lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, C.TIME_UNSET);
// Handle repeated chunks.
long repeatCount = parseLong(parser, KEY_FRAGMENT_REPEAT_COUNT, 1L);
if (repeatCount > 1 && lastChunkDuration == C.TIME_UNSET) {
throw ParserException.createForMalformedManifest(
"Repeated chunk with unspecified duration", /* cause= */ null);
}
for (int i = 1; i < repeatCount; i++) {
chunkIndex++;
startTimes.add(startTime + (lastChunkDuration * i));
}
}
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
type = parseType(parser);
putNormalizedAttribute(KEY_TYPE, type);
if (type == C.TRACK_TYPE_TEXT) {
subType = parseRequiredString(parser, KEY_SUB_TYPE);
} else {
subType = parser.getAttributeValue(null, KEY_SUB_TYPE);
}
putNormalizedAttribute(KEY_SUB_TYPE, subType);
name = parser.getAttributeValue(null, KEY_NAME);
putNormalizedAttribute(KEY_NAME, name);
url = parseRequiredString(parser, KEY_URL);
maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE);
maxHeight = parseInt(parser, KEY_MAX_HEIGHT, Format.NO_VALUE);
displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, Format.NO_VALUE);
displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, Format.NO_VALUE);
language = parser.getAttributeValue(null, KEY_LANGUAGE);
putNormalizedAttribute(KEY_LANGUAGE, language);
timescale = parseInt(parser, KEY_TIME_SCALE, -1);
if (timescale == -1) {
timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
}
startTimes = new ArrayList<>();
}
private int parseType(XmlPullParser parser) throws ParserException {
String value = parser.getAttributeValue(null, KEY_TYPE);
if (value != null) {
if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) {
return C.TRACK_TYPE_AUDIO;
} else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) {
return C.TRACK_TYPE_VIDEO;
} else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) {
return C.TRACK_TYPE_TEXT;
} else {
throw ParserException.createForMalformedManifest(
"Invalid key value[" + value + "]", /* cause= */ null);
}
}
throw new MissingFieldException(KEY_TYPE);
}
@Override
public void addChild(Object child) {
if (child instanceof Format) {
formats.add((Format) child);
}
}
@Override
public Object build() {
Format[] formatArray = new Format[formats.size()];
formats.toArray(formatArray);
return new StreamElement(
baseUri,
url,
type,
subType,
timescale,
name,
maxWidth,
maxHeight,
displayWidth,
displayHeight,
language,
formatArray,
startTimes,
lastChunkDuration);
}
}
private static class QualityLevelParser extends ElementParser {
public static final String TAG = "QualityLevel";
private static final String KEY_INDEX = "Index";
private static final String KEY_BITRATE = "Bitrate";
private static final String KEY_CODEC_PRIVATE_DATA = "CodecPrivateData";
private static final String KEY_SAMPLING_RATE = "SamplingRate";
private static final String KEY_CHANNELS = "Channels";
private static final String KEY_FOUR_CC = "FourCC";
private static final String KEY_TYPE = "Type";
private static final String KEY_SUB_TYPE = "Subtype";
private static final String KEY_LANGUAGE = "Language";
private static final String KEY_NAME = "Name";
private static final String KEY_MAX_WIDTH = "MaxWidth";
private static final String KEY_MAX_HEIGHT = "MaxHeight";
private Format format;
public QualityLevelParser(ElementParser parent, String baseUri) {
super(parent, baseUri, TAG);
}
@Override
public void parseStartTag(XmlPullParser parser) throws ParserException {
Format.Builder formatBuilder = new Format.Builder();
@Nullable String sampleMimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC));
int type = (Integer) getNormalizedAttribute(KEY_TYPE);
if (type == C.TRACK_TYPE_VIDEO) {
List<byte[]> codecSpecificData =
buildCodecSpecificData(parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));
formatBuilder
.setContainerMimeType(MimeTypes.VIDEO_MP4)
.setWidth(parseRequiredInt(parser, KEY_MAX_WIDTH))
.setHeight(parseRequiredInt(parser, KEY_MAX_HEIGHT))
.setInitializationData(codecSpecificData);
} else if (type == C.TRACK_TYPE_AUDIO) {
if (sampleMimeType == null) {
// If we don't know the MIME type, assume AAC.
sampleMimeType = MimeTypes.AUDIO_AAC;
}
int channelCount = parseRequiredInt(parser, KEY_CHANNELS);
int sampleRate = parseRequiredInt(parser, KEY_SAMPLING_RATE);
List<byte[]> codecSpecificData =
buildCodecSpecificData(parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));
if (codecSpecificData.isEmpty() && MimeTypes.AUDIO_AAC.equals(sampleMimeType)) {
codecSpecificData =
Collections.singletonList(
AacUtil.buildAacLcAudioSpecificConfig(sampleRate, channelCount));
}
formatBuilder
.setContainerMimeType(MimeTypes.AUDIO_MP4)
.setChannelCount(channelCount)
.setSampleRate(sampleRate)
.setInitializationData(codecSpecificData);
} else if (type == C.TRACK_TYPE_TEXT) {
@C.RoleFlags int roleFlags = 0;
@Nullable String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE);
if (subType != null) {
switch (subType) {
case "CAPT":
roleFlags = C.ROLE_FLAG_CAPTION;
break;
case "DESC":
roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
break;
default:
break;
}
}
formatBuilder.setContainerMimeType(MimeTypes.APPLICATION_MP4).setRoleFlags(roleFlags);
} else {
formatBuilder.setContainerMimeType(MimeTypes.APPLICATION_MP4);
}
format =
formatBuilder
.setId(parser.getAttributeValue(null, KEY_INDEX))
.setLabel((String) getNormalizedAttribute(KEY_NAME))
.setSampleMimeType(sampleMimeType)
.setAverageBitrate(parseRequiredInt(parser, KEY_BITRATE))
.setLanguage((String) getNormalizedAttribute(KEY_LANGUAGE))
.build();
}
@Override
public Object build() {
return format;
}
private static List<byte[]> buildCodecSpecificData(String codecSpecificDataString) {
ArrayList<byte[]> csd = new ArrayList<>();
if (!TextUtils.isEmpty(codecSpecificDataString)) {
byte[] codecPrivateData = Util.getBytesFromHexString(codecSpecificDataString);
@Nullable byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData);
if (split == null) {
csd.add(codecPrivateData);
} else {
Collections.addAll(csd, split);
}
}
return csd;
}
@Nullable
private static String fourCCToMimeType(String fourCC) {
if (fourCC.equalsIgnoreCase("H264")
|| fourCC.equalsIgnoreCase("X264")
|| fourCC.equalsIgnoreCase("AVC1")
|| fourCC.equalsIgnoreCase("DAVC")) {
return MimeTypes.VIDEO_H264;
} else if (fourCC.equalsIgnoreCase("AAC")
|| fourCC.equalsIgnoreCase("AACL")
|| fourCC.equalsIgnoreCase("AACH")
|| fourCC.equalsIgnoreCase("AACP")) {
return MimeTypes.AUDIO_AAC;
} else if (fourCC.equalsIgnoreCase("TTML") || fourCC.equalsIgnoreCase("DFXP")) {
return MimeTypes.APPLICATION_TTML;
} else if (fourCC.equalsIgnoreCase("ac-3") || fourCC.equalsIgnoreCase("dac3")) {
return MimeTypes.AUDIO_AC3;
} else if (fourCC.equalsIgnoreCase("ec-3") || fourCC.equalsIgnoreCase("dec3")) {
return MimeTypes.AUDIO_E_AC3;
} else if (fourCC.equalsIgnoreCase("dtsc")) {
return MimeTypes.AUDIO_DTS;
} else if (fourCC.equalsIgnoreCase("dtsh") || fourCC.equalsIgnoreCase("dtsl")) {
return MimeTypes.AUDIO_DTS_HD;
} else if (fourCC.equalsIgnoreCase("dtse")) {
return MimeTypes.AUDIO_DTS_EXPRESS;
} else if (fourCC.equalsIgnoreCase("opus")) {
return MimeTypes.AUDIO_OPUS;
}
return null;
}
}
}