/* * 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.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.checkerframework.checker.nullness.compatqual.NullableType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; /** * Parses SmoothStreaming client manifests. * * @see IIS Smooth * Streaming Client Manifest Format */ @UnstableApi public class SsManifestParser implements ParsingLoadable.Parser { 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> 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. * *

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 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 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("") + 5, initDataString.indexOf("")); 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 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 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 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 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 buildCodecSpecificData(String codecSpecificDataString) { ArrayList 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; } } }