public class

SsManifestParser

extends java.lang.Object

implements ParsingLoadable.Parser<SsManifest>

 java.lang.Object

↳androidx.media3.exoplayer.smoothstreaming.manifest.SsManifestParser

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer-smoothstreaming', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer-smoothstreaming
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-exoplayer-smoothstreaming:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

Parses SmoothStreaming client manifests.

See the IIS Smooth Streaming Client Manifest Format

Summary

Constructors
publicSsManifestParser()

Methods
public SsManifestparse(Uri uri, java.io.InputStream inputStream)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public SsManifestParser()

Methods

public SsManifest parse(Uri uri, java.io.InputStream inputStream)

Source

/*
 * 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;
    }
  }
}