com.sonymobile.android.media.internal.ISOBMFFParser.java Source code

Java tutorial

Introduction

Here is the source code for com.sonymobile.android.media.internal.ISOBMFFParser.java

Source

/*
 * Copyright (C) 2014 Sony Mobile Communications Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.sonymobile.android.media.internal;

import static com.sonymobile.android.media.internal.Util.MARLIN_SYSTEM_ID;

import java.io.EOFException;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

import android.media.MediaCodec;
import android.media.MediaCodec.CryptoInfo;
import android.media.MediaFormat;
import android.util.Log;

import org.json.JSONException;

import com.sonymobile.android.media.AudioTrackRepresentation;
import com.sonymobile.android.media.MetaData;
import com.sonymobile.android.media.TrackInfo;
import com.sonymobile.android.media.TrackInfo.TrackType;
import com.sonymobile.android.media.TrackRepresentation;
import com.sonymobile.android.media.VideoTrackRepresentation;
import com.sonymobile.android.media.internal.DataSource.DataAvailability;

public class ISOBMFFParser extends MediaParser {

    private static final boolean LOGS_ENABLED = Configuration.DEBUG || false;

    private static final String TAG = "ISOBMFFParser";

    protected static final int BOX_ID_FTYP = fourCC('f', 't', 'y', 'p');

    protected static final int BOX_ID_UUID = fourCC('u', 'u', 'i', 'd');

    protected static final int BOX_ID_MOOV = fourCC('m', 'o', 'o', 'v');

    protected static final int BOX_ID_MVHD = fourCC('m', 'v', 'h', 'd');

    protected static final int BOX_ID_TRAK = fourCC('t', 'r', 'a', 'k');

    protected static final int BOX_ID_TKHD = fourCC('t', 'k', 'h', 'd');

    protected static final int BOX_ID_MDIA = fourCC('m', 'd', 'i', 'a');

    protected static final int BOX_ID_MDHD = fourCC('m', 'd', 'h', 'd');

    protected static final int BOX_ID_HDLR = fourCC('h', 'd', 'l', 'r');

    protected static final int BOX_ID_MINF = fourCC('m', 'i', 'n', 'f');

    protected static final int BOX_ID_STBL = fourCC('s', 't', 'b', 'l');

    protected static final int BOX_ID_STSD = fourCC('s', 't', 's', 'd');

    protected static final int BOX_ID_AVC1 = fourCC('a', 'v', 'c', '1');

    protected static final int BOX_ID_AVC3 = fourCC('a', 'v', 'c', '3');

    protected static final int BOX_ID_AVCC = fourCC('a', 'v', 'c', 'C');

    protected static final int BOX_ID_STTS = fourCC('s', 't', 't', 's');

    protected static final int BOX_ID_STSZ = fourCC('s', 't', 's', 'z');

    protected static final int BOX_ID_STCO = fourCC('s', 't', 'c', 'o');

    protected static final int BOX_ID_CO64 = fourCC('c', 'o', '6', '4');

    protected static final int BOX_ID_CTTS = fourCC('c', 't', 't', 's');

    protected static final int BOX_ID_STSC = fourCC('s', 't', 's', 'c');

    protected static final int BOX_ID_STSS = fourCC('s', 't', 's', 's');

    protected static final int BOX_ID_MP4V = fourCC('m', 'p', '4', 'v');

    protected static final int BOX_ID_MP4A = fourCC('m', 'p', '4', 'a');

    protected static final int BOX_ID_ESDS = fourCC('e', 's', 'd', 's');

    protected static final int BOX_ID_MDAT = fourCC('m', 'd', 'a', 't');

    protected static final int BOX_ID_MVEX = fourCC('m', 'v', 'e', 'x');

    protected static final int BOX_ID_TREX = fourCC('t', 'r', 'e', 'x');

    protected static final int BOX_ID_MEHD = fourCC('m', 'e', 'h', 'd');

    protected static final int BOX_ID_MOOF = fourCC('m', 'o', 'o', 'f');

    protected static final int BOX_ID_TFHD = fourCC('t', 'f', 'h', 'd');

    protected static final int BOX_ID_TRAF = fourCC('t', 'r', 'a', 'f');

    protected static final int BOX_ID_TRUN = fourCC('t', 'r', 'u', 'n');

    protected static final int BOX_ID_SBGP = fourCC('s', 'b', 'g', 'p');

    protected static final int BOX_ID_SGPD = fourCC('s', 'g', 'p', 'd');

    protected static final int BOX_ID_SUBS = fourCC('s', 'u', 'b', 's');

    protected static final int BOX_ID_SAIZ = fourCC('s', 'a', 'i', 'z');

    protected static final int BOX_ID_SAIO = fourCC('s', 'a', 'i', 'o');

    protected static final int BOX_ID_TFTD = fourCC('t', 'f', 'd', 't');

    protected static final int BOX_ID_SDTP = fourCC('s', 'd', 't', 'p');

    protected static final int BOX_ID_MFRA = fourCC('m', 'f', 'r', 'a');

    protected static final int BOX_ID_TFRA = fourCC('t', 'f', 'r', 'a');

    protected static final int BOX_ID_MFRO = fourCC('m', 'f', 'r', 'o');

    protected static final int BOX_ID_ENCV = fourCC('e', 'n', 'c', 'v');

    protected static final int BOX_ID_ENCA = fourCC('e', 'n', 'c', 'a');

    protected static final int BOX_ID_SINF = fourCC('s', 'i', 'n', 'f');

    protected static final int BOX_ID_FRMA = fourCC('f', 'r', 'm', 'a');

    protected static final int BOX_ID_SCHM = fourCC('s', 'c', 'h', 'm');

    protected static final int BOX_ID_SCHI = fourCC('s', 'c', 'h', 'i');

    protected static final int BOX_ID_EDTS = fourCC('e', 'd', 't', 's');

    protected static final int BOX_ID_ELST = fourCC('e', 'l', 's', 't');

    protected static final int BOX_ID_PSSH = fourCC('p', 's', 's', 'h');

    protected static final int BOX_ID_TENC = fourCC('t', 'e', 'n', 'c');

    protected static final int BOX_ID_STPP = fourCC('s', 't', 'p', 'p');

    protected static final int BOX_ID_SENC = fourCC('s', 'e', 'n', 'c');

    protected static final int BOX_ID_HVCC = fourCC('h', 'v', 'c', 'C');

    protected static final int BOX_ID_HVC1 = fourCC('h', 'v', 'c', '1');

    protected static final int BOX_ID_HEV1 = fourCC('h', 'e', 'v', '1');

    protected static final int BOX_ID_UDTA = fourCC('u', 'd', 't', 'a');

    protected static final int BOX_ID_META = fourCC('m', 'e', 't', 'a');

    // iTunes metadata
    protected static final int BOX_ID_ILST = fourCC('i', 'l', 's', 't');

    protected static final int BOX_ID_ATNAM = fourCC((char) 0xA9, 'n', 'a', 'm'); // title

    protected static final int BOX_ID_ATALB = fourCC((char) 0xA9, 'a', 'l', 'b'); // album

    protected static final int BOX_ID_ATART = fourCC((char) 0xA9, 'A', 'R', 'T'); // artist

    protected static final int BOX_ID_AART = fourCC('a', 'A', 'R', 'T'); // albumartist

    protected static final int BOX_ID_ATDAY = fourCC((char) 0xA9, 'd', 'a', 'y'); // year

    protected static final int BOX_ID_TRKN = fourCC('t', 'r', 'k', 'n'); // tracknumber

    protected static final int BOX_ID_ATGEN = fourCC((char) 0xA9, 'g', 'e', 'n'); // genre

    protected static final int BOX_ID_GNRE = fourCC('g', 'n', 'r', 'e'); // genre

    protected static final int BOX_ID_CPIL = fourCC('c', 'p', 'i', 'l'); // compilation

    protected static final int BOX_ID_ATWRT = fourCC((char) 0xA9, 'w', 'r', 't'); // writer

    protected static final int BOX_ID_DISK = fourCC('d', 'i', 's', 'k'); // disknumber

    protected static final int BOX_ID_DATA = fourCC('d', 'a', 't', 'a');

    // 3GPP metadata
    protected static final int BOX_ID_TITL = fourCC('t', 'i', 't', 'l'); // title

    protected static final int BOX_ID_PERF = fourCC('p', 'e', 'r', 'f'); // artist

    protected static final int BOX_ID_AUTH = fourCC('a', 'u', 't', 'h'); // writer

    protected static final int BOX_ID_ALBM = fourCC('a', 'l', 'b', 'm'); // album
                                                                         // title
                                                                         // and
                                                                         // tracknumber

    protected static final int BOX_ID_YRRC = fourCC('y', 'r', 'r', 'c'); // year

    // ID3v2 metadata
    protected static final int BOX_ID_ID32 = fourCC('I', 'D', '3', '2');

    protected static final int ID3_KEY_COMPILATION = fourCC('T', 'C', 'M', 'P');

    protected static final int ID3_KEY_AUTHOR = fourCC('T', 'E', 'X', 'T');

    protected static final int ID3_KEY_COMPOSER = fourCC('T', 'C', 'O', 'M');

    protected static final int ID3_KEY_DISC_NUMBER = fourCC('T', 'P', 'O', 'S');

    protected static final int HANDLER_TYPE_VIDEO = fourCC('v', 'i', 'd', 'e');

    protected static final int HANDLER_TYPE_AUDIO = fourCC('s', 'o', 'u', 'n');

    protected static final int HANDLER_TYPE_SUBTITLE = fourCC('s', 'u', 'b', 't');

    protected static final int HANDLER_TYPE_HMMP_SUBTITLE = fourCC('G', 'R', 'A', 'P');

    protected static final int HANDLER_TYPE_OBJECT_DESCRIPTOR = fourCC('o', 'd', 's', 'm');

    protected static final int sNALHeaderSize = 4;

    protected static final int AVC_NAL_UNIT_TYPE_IDR_PICTURE = 5;

    protected static final int AVC_NAL_UNIT_TYPE_SPS = 7;

    protected static final int HEVC_NAL_UNIT_TYPE_IDR_PICTURE_W_RADL = 19;

    protected static final int HEVC_NAL_UNIT_TYPE_IDR_PICTURE_N_LP = 20;

    protected static final int HEVC_NAL_UNIT_TYPE_CRA_PICTURE = 21;

    protected IsoTrack mCurrentAudioTrack;

    protected IsoTrack mCurrentVideoTrack;

    protected IsoTrack mCurrentSubtitleTrack;

    protected boolean mIsParsingTrack = false;

    protected boolean mIsFragmented = false;

    protected IsoTrack mCurrentTrack;

    protected long mFileTimescale = 1;

    protected Traf mCurrentTrackFragment;

    protected long mFirstMoofOffset = -1;

    protected long mCurrentMoofOffset = 0;

    protected long mPrevTrunDataSize = 0;

    protected ArrayList<IsoTrack> mMfraTracks;

    protected boolean mHasSampleTable = false;

    protected ArrayDeque<BoxHeader> mCurrentBoxSequence;

    protected boolean mIsPlayReadyProtected = false;

    protected boolean mParseODSMData = false;

    protected MediaFormat mCurrentMediaFormat;

    protected String mCurrentMetaDataKey = null;

    private long mMoofDataSize;

    protected int mCurrentMoofTrackId = -1;

    protected int mCurrentTrackId = -1;

    protected boolean mSkipInsertSamples = false;

    protected boolean mDesiredTrackIdFound = false;

    protected boolean mFoundMfra = false;

    protected boolean mMdatFound = false;

    protected boolean mParsedSencData = false;

    protected int mNALLengthSize;

    private static final int[] ISOBMFF_COMPATIBLE_BRANDS = { fourCC('i', 's', 'o', 'm'), fourCC('m', 'p', '4', '1'),
            fourCC('m', 'p', '4', '2'), fourCC('a', 'v', 'c', '1'), fourCC('3', 'g', 'p', '5'),
            fourCC('h', 'v', 'c', '1') };

    public ISOBMFFParser(DataSource source) {
        super(source);
        if (LOGS_ENABLED)
            Log.v(TAG, "create ISOBMFFParser from source");
    }

    public ISOBMFFParser(String path, int maxBufferSize) {
        super(path, maxBufferSize);
        if (LOGS_ENABLED)
            Log.v(TAG, "create ISOBMFFParser from path");
    }

    public ISOBMFFParser(String path, long offset, long length, int maxBufferSize) {
        super(path, offset, length, maxBufferSize);
        if (LOGS_ENABLED)
            Log.v(TAG, "create ISOBMFFParser from path with length " + length);
    }

    public ISOBMFFParser(FileDescriptor fd, long offset, long length) {
        super(fd, offset, length);
        if (LOGS_ENABLED)
            Log.v(TAG, "create ISOBMFFParser from FileDescriptor");
    }

    protected static int fourCC(char c1, char c2, char c3, char c4) {
        return c1 << 24 | c2 << 16 | c3 << 8 | c4;
    }

    protected String ccruof(long id) {
        StringBuilder sb = new StringBuilder(4);
        sb.append((char) ((id & 0x00000000FF000000) >> 24));
        sb.append((char) ((id & 0x0000000000FF0000) >> 16));
        sb.append((char) ((id & 0x000000000000FF00) >> 8));
        sb.append((char) (id & 0x00000000000000FF));
        return sb.toString();
    }

    @Override
    public boolean parse() {
        if (LOGS_ENABLED)
            Log.v(TAG, "ISOBMFFParser parse");
        boolean parseOK = true;

        initParsing();

        try {
            long sourceLength = mDataSource.length();
            BoxHeader nextHeader;
            mCurrentOffset = 0;
            while (mInitDone == false && (mCurrentOffset < sourceLength || sourceLength == -1) && parseOK) {
                nextHeader = getNextBoxHeader();
                if (nextHeader == null) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "Could not read next box header");
                    parseOK = false;
                } else {
                    parseOK = parseBox(nextHeader);
                }
            }

            if (!mInitDone && mMdatFound && mTracks.size() > 0 && !mIsFragmented) {
                mInitDone = true;
            }

            parseOK = mInitDone;

            if (parseOK && mIsFragmented && !mFoundMfra && sourceLength != -1) {
                long curOffset = mCurrentOffset;
                // read mfra at end of file
                mCurrentOffset = sourceLength - 16;
                nextHeader = getNextBoxHeader();
                if (nextHeader != null && nextHeader.boxType == BOX_ID_MFRO) {
                    byte[] buffer = new byte[4];
                    mDataSource.readAt(sourceLength - buffer.length, buffer, buffer.length);
                    int mfraLength = ((buffer[0] & 0xFF) << 24 | (buffer[1] & 0xFF) << 16 | (buffer[2] & 0xFF) << 8
                            | buffer[3] & 0xFF);
                    mCurrentOffset = sourceLength - mfraLength;
                    nextHeader = getNextBoxHeader();
                    if (nextHeader.boxType == BOX_ID_MFRA) {
                        parseOK = parseBox(nextHeader);
                    } else {
                        if (LOGS_ENABLED)
                            Log.w(TAG, "No mfra at end of file");
                    }
                } else {
                    if (LOGS_ENABLED)
                        Log.w(TAG, "No mfro at end of file");
                }
                mCurrentOffset = curOffset;
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error parsing content", e);
            return false;
        }

        if (mCurrentVideoTrack != null) {
            mMetaDataValues.put(KEY_MIME_TYPE, MimeType.MP4_VIDEO);
        } else {
            mMetaDataValues.put(KEY_MIME_TYPE, MimeType.MP4_AUDIO);
        }

        mMetaDataValues.put(MetaData.KEY_PAUSE_AVAILABLE, 1);
        mMetaDataValues.put(MetaData.KEY_SEEK_AVAILABLE, 1);
        mMetaDataValues.put(MetaData.KEY_NUM_TRACKS, mTracks.size());

        updateAspectRatio();

        if (mCurrentAudioTrack != null) {
            mCurrentAudioTrack.buildSampleTable();
        }

        if (mCurrentVideoTrack != null) {
            mCurrentVideoTrack.buildSampleTable();
        }

        if (mCurrentSubtitleTrack != null) {
            mCurrentSubtitleTrack.buildSampleTable();
        }

        return parseOK;
    }

    protected void updateAspectRatio() {
        if (mCurrentVideoTrack != null) {
            MediaFormat videoFormat = mCurrentVideoTrack.getMediaFormat();
            if (videoFormat.containsKey(MetaData.KEY_SAR_WIDTH)
                    && videoFormat.containsKey(MetaData.KEY_SAR_HEIGHT)) {
                int sarWidth = videoFormat.getInteger(MetaData.KEY_SAR_WIDTH);
                int sarHeight = videoFormat.getInteger(MetaData.KEY_SAR_HEIGHT);

                int adjustedWidth = (int) Math
                        .round(((double) videoFormat.getInteger(MediaFormat.KEY_WIDTH) * sarWidth) / sarHeight);

                addMetaDataValue(KEY_WIDTH, adjustedWidth);
                mCurrentVideoTrack.getMetaData().addValue(KEY_WIDTH, adjustedWidth);
            }
        }
    }

    protected void initParsing() {
        mCurrentOffset = 0;
        mCurrentBoxSequence = new ArrayDeque<BoxHeader>(10);
    }

    @Override
    public MediaFormat getTrackMediaFormat(int trackIndex) {
        if (trackIndex < 0 || trackIndex >= mTracks.size()) {
            return null;
        }
        return ((IsoTrack) mTracks.get(trackIndex)).getMediaFormat();
    }

    @Override
    public synchronized AccessUnit dequeueAccessUnit(TrackType type) {
        IsoTrack currentTrack;

        if (type == TrackType.AUDIO && mCurrentAudioTrack != null) {
            currentTrack = mCurrentAudioTrack;
        } else if (type == TrackType.VIDEO && mCurrentVideoTrack != null) {
            currentTrack = mCurrentVideoTrack;
        } else if (type == TrackType.SUBTITLE && mCurrentSubtitleTrack != null) {
            currentTrack = mCurrentSubtitleTrack;
        } else {
            return null;
        }

        return currentTrack.dequeueAccessUnit(mIsFragmented);
    }

    protected BoxHeader getNextBoxHeader() {
        long startOffset = mCurrentOffset;
        byte[] buffer = new byte[8];
        try {
            if (mDataSource.readAt(mCurrentOffset, buffer, 8) != 8) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "could not read 8 bytes for header");
                return null;
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error reading next header from source", e);
            return null;
        }

        mCurrentOffset += 8;

        if (LOGS_ENABLED)
            Log.v(TAG, "boxSize bytes = " + buffer[0] + " " + buffer[1] + " " + buffer[2] + " " + buffer[3] + " ");

        long boxSize = (buffer[0] & 0xFF) << 24 | (buffer[1] & 0xFF) << 16 | (buffer[2] & 0xFF) << 8
                | buffer[3] & 0xFF;
        int boxType = (buffer[4] & 0xFF) << 24 | (buffer[5] & 0xFF) << 16 | (buffer[6] & 0xFF) << 8
                | buffer[7] & 0xFF;
        int boxHeaderSize = 8;

        if (boxSize == 1) {
            try {
                if (mDataSource.readAt(mCurrentOffset, buffer, 8) != 8) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "could not read 8 bytes for extended size");
                    return null;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Error while reading extended box size", e);
            }
            mCurrentOffset += 8;
            boxSize = ((long) (buffer[0] & 0xFF) << 56 | (long) (buffer[1] & 0xFF) << 48
                    | (long) (buffer[2] & 0xFF) << 40 | (long) (buffer[3] & 0xFF) << 32 | (buffer[4] & 0xFF) << 24
                    | (buffer[5] & 0xFF) << 16 | (buffer[6] & 0xFF) << 8 | buffer[7] & 0xFF);
            boxSize -= 8;
            boxHeaderSize += 8;
        }

        boxSize -= 8;

        BoxHeader boxHeader = new BoxHeader();
        boxHeader.boxHeaderSize = boxHeaderSize;
        boxHeader.boxDataSize = boxSize;
        boxHeader.boxType = boxType;
        boxHeader.startOffset = startOffset;

        if (LOGS_ENABLED)
            Log.v(TAG, "read box " + ccruof(boxHeader.boxType) + " with size " + boxSize + " from offset "
                    + startOffset);

        return boxHeader;
    }

    protected boolean parseBox(BoxHeader header) {
        if (header == null) {
            return false;
        }
        mCurrentBoxSequence.add(header);

        if (LOGS_ENABLED)
            Log.v(TAG, "parse box " + ccruof(header.boxType) + " with size " + header.boxDataSize);

        boolean parseOK = true;
        long boxEndOffset = mCurrentOffset + header.boxDataSize;
        if (header.boxType == BOX_ID_FTYP) {

        } else if (header.boxType == BOX_ID_MOOV) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            // Merge tracks from moov and mfra
            if (mMfraTracks != null) {
                int numTracks = mTracks.size();
                int numMfraTracks = mMfraTracks.size();
                if (numMfraTracks > 0) {
                    for (int i = 0; i < numTracks; i++) {
                        IsoTrack track = (IsoTrack) mTracks.get(i);
                        for (int j = 0; j < numMfraTracks; j++) {
                            IsoTrack t = mMfraTracks.get(j);
                            if (t.getTrackId() == track.getTrackId()) {
                                track.setTfraList(t.getTfraList());
                                mMfraTracks.remove(j);
                                break;
                            }
                        }
                    }
                }
                mMfraTracks = null;
            }

            // Check for unsupported tracks
            int numTracks = mTracks.size();

            if (LOGS_ENABLED)
                Log.v(TAG,
                        numTracks + " tracks, " + "Video track " + getSelectedTrackIndex(TrackType.VIDEO)
                                + " Audio track " + getSelectedTrackIndex(TrackType.AUDIO) + " Subtitle track "
                                + getSelectedTrackIndex(TrackType.SUBTITLE));

            for (int i = 0; i < numTracks; i++) {
                IsoTrack track = (IsoTrack) mTracks.get(i);
                if (track.getMediaFormat() == null) {
                    if (LOGS_ENABLED)
                        Log.v(TAG, "Track " + i + " is unhandled, type " + track.getTrackType());
                    track.setTrackType(TrackType.UNKNOWN);
                } else {
                    if (LOGS_ENABLED)
                        Log.v(TAG, "Track " + i + " of type " + track.getTrackType() + " is OK");
                    track.setTrackIndex(i);
                }
            }

        } else if (header.boxType == BOX_ID_MVHD) {
            parseOK = parseMvhd(header);
        } else if (header.boxType == BOX_ID_TRAK) {
            mIsParsingTrack = true;
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            if (mParseODSMData) {
                if (!parseODSMData(mCurrentTrack)) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "Error while parsing ODSM track");
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
                mParseODSMData = false;
            }
            mIsParsingTrack = false;
        } else if (header.boxType == BOX_ID_TKHD) {
            parseOK = readTkhd(header);
        } else if (header.boxType == BOX_ID_MDIA) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_MDHD) {
            parseOK = parseMdhd(header);
        } else if (header.boxType == BOX_ID_HDLR) {
            parseOK = parseHdlr(header);
        } else if (header.boxType == BOX_ID_MINF) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }

            if (parseOK) {
                if (mCurrentTrack.getTrackType() == TrackType.AUDIO && mCurrentAudioTrack == null) {
                    if (LOGS_ENABLED)
                        Log.v(TAG, "Setting audio track to " + mCurrentTrack.getTrackId());
                    mCurrentAudioTrack = mCurrentTrack;
                } else if (mCurrentTrack.getTrackType() == TrackType.VIDEO && mCurrentVideoTrack == null) {
                    if (LOGS_ENABLED)
                        Log.v(TAG, "Setting video track to " + mCurrentTrack.getTrackId());
                    mCurrentVideoTrack = mCurrentTrack;
                }
            } else {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Error parsing minf boxes");
            }
        } else if (header.boxType == BOX_ID_STBL) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            if (parseOK) {
                IsoTrack currentTrack = (IsoTrack) mTracks.get(mTracks.size() - 1);
                SampleTable sampleTable = currentTrack.getSampleTable();
                sampleTable.setTimescale(currentTrack.getTimeScale());

                if (sampleTable.calculateSampleCountAndDuration() == false) {
                    if (LOGS_ENABLED)
                        Log.w(TAG, "Error while calculating sample count and duration");
                }
                int sampleCount = sampleTable.getSampleCount();
                if (sampleCount > 0) {
                    mHasSampleTable = true;
                }
                long trackDurationUs = currentTrack.getDurationUs();
                if (trackDurationUs > 0) {
                    float frameRate = (sampleCount * 1000000.0f / trackDurationUs);
                    mCurrentTrack.getMetaData().addValue(KEY_FRAME_RATE, frameRate);
                } else {
                    mCurrentTrack.getMetaData().addValue(KEY_FRAME_RATE, 0f);
                }
            }
        } else if (header.boxType == BOX_ID_STSD) {
            // skip 4 for version and flags
            // skip 4 for entry_count
            mCurrentOffset += 8;
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }

            if (mCurrentTrack.getMediaFormat() == null) {
                if (LOGS_ENABLED)
                    Log.w(TAG, "Error parsing handler in 'stsd' box");
                mCurrentTrack.setTrackType(TrackType.UNKNOWN);
            }
        } else if (header.boxType == BOX_ID_AVC1 || header.boxType == BOX_ID_AVC3) {
            byte[] data = new byte[78];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Error while parsing 'avc1' box", e);
                mCurrentBoxSequence.removeLast();
                return false;
            }

            mCurrentMediaFormat = new MediaFormat();

            parseVisualSampleEntry(data);

            mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.AVC);
            // TODO: Update this when we add support for nalSize other than 4
            mCurrentMediaFormat.setInteger("nal-size", 4);
            mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.AVC);

            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);

        } else if (header.boxType == BOX_ID_AVCC) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Error while parsing 'avcc' box", e);
                mCurrentBoxSequence.removeLast();
                return false;
            }

            AvccData avccData = parseAvcc(data);
            if (avccData == null) {
                return false;
            }
            ByteBuffer csd0 = ByteBuffer.wrap(avccData.spsBuffer.array());
            ByteBuffer csd1 = ByteBuffer.wrap(avccData.ppsBuffer.array());
            mCurrentMediaFormat.setByteBuffer("csd-0", csd0);
            mCurrentMediaFormat.setByteBuffer("csd-1", csd1);
            mCurrentMediaFormat.setInteger("nal-length-size", mNALLengthSize);

            parseSPS(avccData.spsBuffer.array());
        } else if (header.boxType == BOX_ID_STTS) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'stts' box", e);
            }
            mCurrentTrack.getSampleTable().setSttsData(data);
        } else if (header.boxType == BOX_ID_STSZ) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'stsz' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            mCurrentTrack.getSampleTable().setStszData(data);
        } else if (header.boxType == BOX_ID_CTTS) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'ctts' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            mCurrentTrack.getSampleTable().setCttsData(data);
        } else if (header.boxType == BOX_ID_STSC) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'stsc' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            mCurrentTrack.getSampleTable().setStscData(data);
        } else if (header.boxType == BOX_ID_STSS) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'stss' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            mCurrentTrack.getSampleTable().setStssData(data);
        } else if (header.boxType == BOX_ID_STCO) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'stco' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            mCurrentTrack.getSampleTable().setStcoData(data);
        } else if (header.boxType == BOX_ID_CO64) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'co64' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            mCurrentTrack.getSampleTable().setCo64Data(data);
        } else if (header.boxType == BOX_ID_MP4V) {
            byte[] data = new byte[78];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'mp4v' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }

            mCurrentMediaFormat = new MediaFormat();

            // mp4v is a type of VisualSampleEntry
            parseVisualSampleEntry(data);

            mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.MPEG4_VISUAL);
            mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.MPEG4_VISUAL);

            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);
        } else if (header.boxType == BOX_ID_MP4A) {
            byte[] data = new byte[28];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'mp4a' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }

            mCurrentMediaFormat = new MediaFormat();

            parseAudioSampleEntry(data);

            mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.AAC);
            mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.AAC);

            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);
        } else if (header.boxType == BOX_ID_ESDS) {
            // skip 4 for version and flags
            mCurrentOffset += 4;
            byte[] data = new byte[(int) header.boxDataSize - 4];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'esds' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
            parseOK = parseESDS(data);
        } else if (header.boxType == BOX_ID_STPP) {
            mCurrentOffset += header.boxDataSize;

            mCurrentMediaFormat = new MediaFormat();

            mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.TTML);
            mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.TTML);

            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);
        } else if (header.boxType == BOX_ID_MVEX) {
            if (LOGS_ENABLED)
                Log.v(TAG, "found 'mvex', setting fragmented to true");
            mIsFragmented = true;
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_MEHD) {
            int versionFlags = 0;
            try {
                versionFlags = mDataSource.readInt();
                int version = (versionFlags >> 24) & 0xFF;

                long durationTicks = 0;
                if (version == 1) {
                    durationTicks = mDataSource.readLong();
                } else {
                    durationTicks = mDataSource.readInt();
                }
                addMetaDataValue(KEY_DURATION, durationTicks * 1000 / mFileTimescale);
            } catch (EOFException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "EOFException while parsing 'mvex' box", e);
                mCurrentBoxSequence.removeLast();

                return false;
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'mehd' box", e);
                mCurrentBoxSequence.removeLast();

                return false;
            }
        } else if (header.boxType == BOX_ID_TREX) {
            try {
                Trex newTrex = new Trex();
                mDataSource.skipBytes(4); // version and flags
                int trackId = mDataSource.readInt();
                mDataSource.skipBytes(4); // Skip Default Sample Description Index
                newTrex.defaultSampleDuration = mDataSource.readInt();
                newTrex.defaultSampleSize = mDataSource.readInt();
                mDataSource.skipBytes(4); // Skip Default Sample Flags

                IsoTrack track = null;
                int numTracks = mTracks.size();
                for (int i = 0; i < numTracks; i++) {
                    IsoTrack t = (IsoTrack) (mTracks.get(i));
                    if (t.getTrackId() == trackId) {
                        track = t;
                        break;
                    }
                }

                if (track == null) {
                    track = (IsoTrack) createTrack();
                    track.setTrackId(trackId);
                    mTracks.add(track);
                }

                if (track != null) {
                    track.setTrex(newTrex);
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'trex' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }
        } else if (header.boxType == BOX_ID_MOOF) {
            if (mFirstMoofOffset == -1) {
                mIsFragmented = true;
                mInitDone = true;
                mFirstMoofOffset = header.startOffset;
                mCurrentBoxSequence.removeLast();
                return true;
            }
            mCurrentMoofOffset = header.startOffset;
            mMoofDataSize = 0;
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_TRAF) {
            mParsedSencData = false;
            mCurrentTrackFragment = new Traf();
            mPrevTrunDataSize = 0;
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_TFHD) {
            parseOK = parseTfhd(header);
        } else if (header.boxType == BOX_ID_TRUN) {
            parseOK = parseTrun(header);
        } else if (header.boxType == BOX_ID_MFRA) {
            if (!mFoundMfra) {
                mMfraTracks = new ArrayList<ISOBMFFParser.IsoTrack>(2);
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mFoundMfra = true;
            }
        } else if (header.boxType == BOX_ID_TFRA) {
            parseOK = parseTfra(header);
        } else if (header.boxType == BOX_ID_ENCA) {
            byte[] data = new byte[28];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'enca' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }

            mCurrentMediaFormat = new MediaFormat();

            parseAudioSampleEntry(data);

            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);
        } else if (header.boxType == BOX_ID_ENCV) {
            byte[] data = new byte[78];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'encv' box", e);

                mCurrentBoxSequence.removeLast();
                return false;
            }

            mCurrentMediaFormat = new MediaFormat();

            parseVisualSampleEntry(data);

            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);
        } else if (header.boxType == BOX_ID_FRMA) {
            try {
                int dataFormat = mDataSource.readInt();
                if (dataFormat == BOX_ID_AVC1) {
                    mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.AVC);
                    mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.AVC);
                } else if (dataFormat == BOX_ID_HVC1) {
                    mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.HEVC);
                    mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.HEVC);
                } else if (dataFormat == BOX_ID_MP4V) {
                    mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.MPEG4_VISUAL);
                    mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.MPEG4_VISUAL);
                } else if (dataFormat == BOX_ID_MP4A) {
                    mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.AAC);
                    mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.AAC);
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Exception while parsing 'frma' box", e);
                mCurrentBoxSequence.removeLast();
                return false;
            }
        } else if (header.boxType == BOX_ID_SCHM) {
            try {
                int versionFlags = mDataSource.readInt();
                mDataSource.skipBytes(8); // scheme_type and scheme_version
                if ((versionFlags & 0x01) != 0) {
                    // TODO read scheme_uri if we're interested
                    // byte[] data = new byte[(int)header.boxDataSize - 12];
                    // mDataSource.read(data);
                    mDataSource.skipBytes(header.boxDataSize - 12);
                }
            } catch (EOFException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Error parsing 'schm' box", e);

                mCurrentBoxSequence.removeLast();
                parseOK = false;
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "Error parsing 'schm' box", e);

                mCurrentBoxSequence.removeLast();
                parseOK = false;
            }
        } else if (header.boxType == BOX_ID_SCHI) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_EDTS) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_ELST) {
            parseOK = parseElst(header);
        } else if (header.boxType == BOX_ID_PSSH) {
            parseOK = parsePsshData(header);
        } else if (header.boxType == BOX_ID_TENC) {
            try {
                // Skip version, flags and algorithm id
                mDataSource.skipBytes(7);
                int ivSize = mDataSource.readByte();
                byte[] kID = new byte[16];
                mDataSource.read(kID);
                mCurrentTrack.setDefaultEncryptionData(ivSize, kID);
                parseOK = true;
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'tenc' box", e);
                parseOK = false;
            }
        } else if (header.boxType == BOX_ID_SENC) {
            if (mCurrentMoofTrackId == mCurrentTrackId && !mSkipInsertSamples && !mParsedSencData) {
                mParsedSencData = true;
                try {
                    int versionFlags = mDataSource.readInt();

                    int sampleCount = mDataSource.readInt();

                    ArrayList<CryptoInfo> cryptoInfos = new ArrayList<CryptoInfo>(sampleCount);

                    for (int i = 0; i < sampleCount; i++) {
                        CryptoInfo info = new CryptoInfo();
                        info.mode = MediaCodec.CRYPTO_MODE_AES_CTR;
                        info.iv = new byte[16];
                        if (mCurrentTrack.mDefaultIVSize == 16) {
                            mDataSource.read(info.iv);
                        } else {
                            // pad IV data to 128 bits
                            byte[] iv = new byte[8];
                            mDataSource.read(iv);
                            System.arraycopy(iv, 0, info.iv, 0, 8);
                        }
                        if ((versionFlags & 0x00000002) > 0) {
                            short subSampleCount = mDataSource.readShort();
                            info.numSubSamples = subSampleCount;
                            info.numBytesOfClearData = new int[subSampleCount];
                            info.numBytesOfEncryptedData = new int[subSampleCount];
                            for (int j = 0; j < subSampleCount; j++) {
                                info.numBytesOfClearData[j] = mDataSource.readShort();
                                info.numBytesOfEncryptedData[j] = mDataSource.readInt();
                            }
                        } else {
                            info.numSubSamples = 1;
                            info.numBytesOfClearData = new int[1];
                            info.numBytesOfClearData[0] = 0;
                            info.numBytesOfEncryptedData = new int[1];
                            info.numBytesOfEncryptedData[0] = -1;
                        }

                        if (info.numBytesOfClearData[0] == 0 && mCurrentTrack.getTrackType() == TrackType.VIDEO) {
                            info.iv[15] = (byte) mNALLengthSize;
                        }

                        cryptoInfos.add(info);
                    }

                    mCurrentTrack.addCryptoInfos(cryptoInfos);
                } catch (EOFException e) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "Error parsing 'senc' box", e);

                    mCurrentBoxSequence.removeLast();
                    parseOK = false;
                } catch (IOException e) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "Error parsing 'senc' box", e);

                    mCurrentBoxSequence.removeLast();
                    parseOK = false;
                }
            }
        } else if (header.boxType == BOX_ID_SINF) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_HVC1 || header.boxType == BOX_ID_HEV1) {
            byte[] data = new byte[78];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'hvc1' box", e);
                return false;
            }

            mCurrentMediaFormat = new MediaFormat();

            mCurrentMediaFormat.setString(MediaFormat.KEY_MIME, MimeType.HEVC);
            mCurrentTrack.getMetaData().addValue(KEY_MIME_TYPE, MimeType.HEVC);

            parseVisualSampleEntry(data);

            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
            mCurrentTrack.addSampleDescriptionEntry(mCurrentMediaFormat);
        } else if (header.boxType == BOX_ID_HVCC) {
            byte[] data = new byte[(int) header.boxDataSize];
            try {
                if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                    mCurrentBoxSequence.removeLast();
                    return false;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while parsing 'hvcc' box", e);
                return false;
            }

            byte[] hvccData = parseHvcc(data);
            if (hvccData == null) {
                return false;
            }
            ByteBuffer csd0 = ByteBuffer.wrap(hvccData);
            mCurrentMediaFormat.setByteBuffer("csd-0", csd0);
            mCurrentMediaFormat.setInteger("nal-length-size", mNALLengthSize);
        } else if (header.boxType == BOX_ID_UDTA) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_META) {
            mCurrentOffset += 4; // skip version and flags
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_ILST) {
            while (mCurrentOffset < boxEndOffset && parseOK) {
                BoxHeader nextBoxHeader = getNextBoxHeader();
                parseOK = parseBox(nextBoxHeader);
            }
        } else if (header.boxType == BOX_ID_ATNAM) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_TITLE;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_ATALB) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_ALBUM;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_ATART) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_ARTIST;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_AART) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_ALBUM_ARTIST;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_ATDAY) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_YEAR;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_TRKN) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_TRACK_NUMBER;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_ATGEN || header.boxType == BOX_ID_GNRE) {
            mCurrentMetaDataKey = KEY_GENRE;
            if (boxIsUnder(BOX_ID_ILST)) {
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
            } else { // 3gpp metadata value
                try {
                    mDataSource.skipBytes(4); // skip version and flags
                    mDataSource.skipBytes(2); // skip language code
                    byte[] buffer = new byte[(int) (header.boxDataSize - 6)];
                    mDataSource.read(buffer);
                    String metaDataValue = null;
                    if ((0xFF & buffer[0]) == 0xFF && (0xFF & buffer[1]) == 0xFE
                            || (0xFF & buffer[0]) == 0xFE && (0xFF & buffer[1]) == 0xFF) {
                        metaDataValue = new String(buffer, 0, buffer.length - 2, StandardCharsets.UTF_16);
                    } else {
                        metaDataValue = new String(buffer, 0, buffer.length - 1, StandardCharsets.UTF_8);
                    }
                    addMetaDataValue(mCurrentMetaDataKey, metaDataValue);
                } catch (IOException e) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "IOException parsing 'gnre' box", e);
                    parseOK = false;
                }
            }
            mCurrentMetaDataKey = null;
        } else if (header.boxType == BOX_ID_CPIL) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_COMPILATION;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_ATWRT) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_WRITER;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_DISK) {
            if (boxIsUnder(BOX_ID_ILST)) {
                mCurrentMetaDataKey = KEY_DISC_NUMBER;
                while (mCurrentOffset < boxEndOffset && parseOK) {
                    BoxHeader nextBoxHeader = getNextBoxHeader();
                    parseOK = parseBox(nextBoxHeader);
                }
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_DATA) {
            parseOK = parseDataBox(header);
        } else if (header.boxType == BOX_ID_ID32) {
            parseOK = parseID3(header);
        } else if (header.boxType == BOX_ID_TITL) {
            if (!mMetaDataValues.containsKey(KEY_TITLE)) {
                mCurrentMetaDataKey = KEY_TITLE;
                parseOK = parse3GPPMetaDataString(header);
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_PERF) {
            if (!mMetaDataValues.containsKey(KEY_ARTIST)) {
                mCurrentMetaDataKey = KEY_ARTIST;
                parseOK = parse3GPPMetaDataString(header);
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_AUTH) {
            if (!mMetaDataValues.containsKey(KEY_AUTHOR)) {
                mCurrentMetaDataKey = KEY_AUTHOR;
                parseOK = parse3GPPMetaDataString(header);
                mCurrentMetaDataKey = null;
            }
        } else if (header.boxType == BOX_ID_ALBM) {
            if (!mMetaDataValues.containsKey(KEY_ALBUM)) {
                try {
                    mDataSource.skipBytes(4); // skip version and flags
                    mDataSource.skipBytes(2); // skip language code
                    byte[] buffer = new byte[(int) (header.boxDataSize - 6)];
                    mDataSource.read(buffer);
                    String metaDataValue = null;
                    if ((0xFF & buffer[0]) == 0xFF && (0xFF & buffer[1]) == 0xFE) {
                        if (buffer[buffer.length - 3] == 0 && buffer[buffer.length - 2] == 0) {
                            if (!mMetaDataValues.containsKey(KEY_TRACK_NUMBER)) {
                                String trackNumber = Byte.toString(buffer[buffer.length - 1]);
                                addMetaDataValue(KEY_TRACK_NUMBER, trackNumber);
                            }
                            metaDataValue = new String(buffer, 0, buffer.length - 3, StandardCharsets.UTF_16);
                        } else {
                            metaDataValue = new String(buffer, StandardCharsets.UTF_16);
                        }
                    } else if ((0xFF & buffer[0]) == 0xFE && (0xFF & buffer[1]) == 0xFF) {
                        if (buffer[buffer.length - 3] == 0 && buffer[buffer.length - 2] == 0) {
                            if (!mMetaDataValues.containsKey(KEY_TRACK_NUMBER)) {
                                String trackNumber = Byte.toString(buffer[buffer.length - 1]);
                                addMetaDataValue(KEY_TRACK_NUMBER, trackNumber);
                            }
                            metaDataValue = new String(buffer, 0, buffer.length - 3, StandardCharsets.UTF_16);
                        } else {
                            metaDataValue = new String(buffer, 0, buffer.length - 2, StandardCharsets.UTF_16);
                        }
                    } else {
                        if (buffer[buffer.length - 2] == 0) {
                            if (!mMetaDataValues.containsKey(KEY_TRACK_NUMBER)) {
                                String trackNumber = Byte.toString(buffer[buffer.length - 1]);
                                addMetaDataValue(KEY_TRACK_NUMBER, trackNumber);
                            }
                            metaDataValue = new String(buffer, 0, buffer.length - 2, StandardCharsets.UTF_8);
                        } else {
                            metaDataValue = new String(buffer, 0, buffer.length - 1, StandardCharsets.UTF_8);
                        }
                    }
                    addMetaDataValue(KEY_ALBUM, metaDataValue);
                } catch (IOException e) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "IOException parsing 'albm' box", e);
                    parseOK = false;
                }
            }
        } else if (header.boxType == BOX_ID_YRRC) {
            try {
                mDataSource.skipBytes(4); // skip version and flags
                if (header.boxDataSize > 6) {
                    // This should be a 16 bit int according to spec, but some
                    // files have this as a string
                    mDataSource.skipBytes(2); // skip language code
                    byte[] buffer = new byte[(int) (header.boxDataSize - 6)];
                    mDataSource.read(buffer);
                    String metaDataValue = null;
                    if ((0xFF & buffer[0]) == 0xFF && (0xFF & buffer[1]) == 0xFE
                            || (0xFF & buffer[0]) == 0xFE && (0xFF & buffer[1]) == 0xFF) {
                        metaDataValue = new String(buffer, 0, buffer.length - 2, StandardCharsets.UTF_16);
                    } else {
                        metaDataValue = new String(buffer, 0, buffer.length - 1, StandardCharsets.UTF_8);
                    }
                    addMetaDataValue(KEY_YEAR, metaDataValue);
                } else {
                    int year = mDataSource.readShort();
                    String metaDataValue = Integer.toString(year);
                    addMetaDataValue(KEY_YEAR, metaDataValue);
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException parsing 'yrrc' box", e);
                parseOK = false;
            }
        } else if (header.boxType == BOX_ID_MDAT) {
            if (mTracks.size() > 0 && !mIsFragmented) {
                mInitDone = true;
            } else if (mIsFragmented && mFirstMoofOffset != -1) {
                mInitDone = true;
            } else {
                mMdatFound = true;
            }
        } else {
            long skipSize = header.boxDataSize;
            try {
                while (skipSize > Integer.MAX_VALUE) {
                    mDataSource.skipBytes(Integer.MAX_VALUE);
                    skipSize -= Integer.MAX_VALUE;
                }
                if (skipSize > 0) {
                    mDataSource.skipBytes(skipSize);
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "could not skip box");

                mCurrentBoxSequence.removeLast();
                parseOK = false;
            }
        }
        mCurrentOffset = boxEndOffset;
        mCurrentBoxSequence.removeLast();
        return parseOK;
    }

    protected boolean boxIsUnder(int boxType) {
        Iterator<BoxHeader> iterator = mCurrentBoxSequence.iterator();
        while (iterator.hasNext()) {
            BoxHeader header = iterator.next();
            if (header.boxType == boxType) {
                return true;
            }
        }
        return false;
    }

    protected boolean parseID3(BoxHeader header) {
        long id3size = header.boxDataSize;
        long currentOffset = 0;

        try {
            mDataSource.skipBytes(6); // skip version, flags and language

            byte[] id3header = new byte[3];
            mDataSource.read(id3header);

            if (id3header[0] == 0x49 && // 'I'
                    id3header[1] == 0x44 && // 'D'
                    id3header[2] == 0x33 // '3'
            ) {
                mDataSource.skipBytes(2); // skip major version and revision

                int flags = mDataSource.readByte();
                mDataSource.skipBytes(4); // padding

                currentOffset += 16;

                if ((flags & (1 << 6)) > 0) {
                    // Extended ID3 header
                    int extendedHeaderSize = mDataSource.readInt();
                    mDataSource.skipBytes(extendedHeaderSize);

                    currentOffset += 4 + extendedHeaderSize;
                }

                while (currentOffset + 10 < id3size) {
                    int frameId = mDataSource.readInt();
                    int frameSize = mDataSource.readInt();

                    mDataSource.skipBytes(2); // skip flags

                    currentOffset += 10 + frameSize;

                    if (frameId == ID3_KEY_COMPILATION) {
                        if (!mMetaDataValues.containsKey(KEY_COMPILATION)) {
                            String id3String = readID3String(frameSize);
                            addMetaDataValue(KEY_COMPILATION, id3String);
                        } else {
                            mDataSource.skipBytes(frameSize);
                        }
                    } else if (frameId == ID3_KEY_AUTHOR) {
                        if (!mMetaDataValues.containsKey(KEY_AUTHOR)) {
                            String id3String = readID3String(frameSize);
                            addMetaDataValue(KEY_AUTHOR, id3String);
                        } else {
                            mDataSource.skipBytes(frameSize);
                        }
                    } else if (frameId == ID3_KEY_COMPOSER) {
                        if (!mMetaDataValues.containsKey(KEY_COMPOSER)) {
                            String id3String = readID3String(frameSize);
                            addMetaDataValue(KEY_COMPOSER, id3String);
                        } else {
                            mDataSource.skipBytes(frameSize);
                        }
                    } else if (frameId == ID3_KEY_DISC_NUMBER) {
                        if (!mMetaDataValues.containsKey(KEY_DISC_NUMBER)) {
                            String id3String = readID3String(frameSize);
                            addMetaDataValue(KEY_DISC_NUMBER, id3String);
                        } else {
                            mDataSource.skipBytes(frameSize);
                        }
                    } else {
                        mDataSource.skipBytes(frameSize);
                    }
                }

            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException parsing id3 box", e);
            return false;
        }

        return true;
    }

    private String readID3String(int frameSize) {
        String metadataString = null;
        try {
            int encoding = mDataSource.readByte();
            if (frameSize > 1) {
                if (encoding == 0) {
                    // ISO 8859-1
                    byte[] buffer = new byte[frameSize - 1];
                    mDataSource.read(buffer);
                    metadataString = new String(buffer, StandardCharsets.ISO_8859_1);
                } else if (encoding == 2) {
                    // UTF-16
                    int bom = mDataSource.readShort();
                    byte[] buffer = new byte[frameSize - 3];
                    short little_endian = (short) 0xFFFE;
                    mDataSource.read(buffer);
                    if (bom == little_endian) {
                        metadataString = new String(buffer, StandardCharsets.UTF_16LE);
                    } else {
                        metadataString = new String(buffer, StandardCharsets.UTF_16BE);
                    }
                } else if (encoding == 3) {
                    // UTF-8
                    byte[] buffer = new byte[frameSize - 1];
                    mDataSource.read(buffer);
                    metadataString = new String(buffer, StandardCharsets.UTF_8);
                } else {
                    // UCS-2
                    int bom = mDataSource.readShort();
                    byte[] buffer = new byte[frameSize - 3];
                    mDataSource.read(buffer);
                    short little_endian = (short) 0xFFFE;
                    if (bom == little_endian) {
                        for (int i = 0; i < buffer.length; i += 2) {
                            byte tempByte = buffer[i];
                            buffer[i] = buffer[i + 1];
                            buffer[i + 1] = tempByte;
                        }
                    }
                    metadataString = new String(buffer, "UCS-2");
                }
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOEception reading ID3 string", e);
        }
        return metadataString;
    }

    private boolean parse3GPPMetaDataString(BoxHeader header) {
        try {
            mDataSource.skipBytes(4); // skip version and flags
            mDataSource.skipBytes(2); // skip language code
            byte[] buffer = new byte[(int) (header.boxDataSize - 6)];
            mDataSource.read(buffer);
            String metaDataValue = null;
            if ((0xFF & buffer[0]) == 0xFF && (0xFF & buffer[1]) == 0xFE
                    || (0xFF & buffer[0]) == 0xFE && (0xFF & buffer[1]) == 0xFF) {
                metaDataValue = new String(buffer, 0, buffer.length - 2, StandardCharsets.UTF_16);
            } else {
                metaDataValue = new String(buffer, 0, buffer.length - 1, StandardCharsets.UTF_8);
            }
            addMetaDataValue(mCurrentMetaDataKey, metaDataValue);
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException parsing 'titl' box", e);
            return false;
        }
        return true;
    }

    protected boolean parseODSMData(IsoTrack odsmTrack) {
        // empty implementation, interested subclasses should implement it
        return true;
    }

    protected boolean parsePsshData(BoxHeader header) {

        try {
            byte[] psshData = new byte[(int) header.boxDataSize];
            mDataSource.read(psshData);

            String systemIDString = Util.bytesToHex(psshData, 4, 16);
            byte[] systemId = new byte[16];
            System.arraycopy(psshData, 4, systemId, 0, 16);

            if (systemIDString.equals(MARLIN_SYSTEM_ID)) {
                int boxSize = (int) (header.boxHeaderSize + header.boxDataSize);
                byte[] psshBox = new byte[(int) (header.boxHeaderSize + header.boxDataSize) + 4];
                psshBox[0] = (byte) ((boxSize & 0xFF000000) >> 24);
                psshBox[1] = (byte) ((boxSize & 0x00FF0000) >> 16);
                psshBox[2] = (byte) ((boxSize & 0x0000FF00) >> 8);
                psshBox[3] = (byte) (boxSize & 0x000000FF);
                psshBox[4] = (byte) ((boxSize & 0xFF000000) >> 24);
                psshBox[5] = (byte) ((boxSize & 0x00FF0000) >> 16);
                psshBox[6] = (byte) ((boxSize & 0x0000FF00) >> 8);
                psshBox[7] = (byte) (boxSize & 0x000000FF);
                psshBox[8] = (byte) ((header.boxType & 0xFF000000) >> 24);
                psshBox[9] = (byte) ((header.boxType & 0x00FF0000) >> 16);
                psshBox[10] = (byte) ((header.boxType & 0x0000FF00) >> 8);
                psshBox[11] = (byte) (header.boxType & 0x000000FF);

                System.arraycopy(psshData, 0, psshBox, 12, psshData.length);

                byte[][] kids = new byte[mTracks.size()][];

                for (int i = 0; i < mTracks.size(); i++) {
                    kids[i] = ((IsoTrack) mTracks.get(i)).mKID;
                }

                String psshJson = Util.getMarlinPSSHTable(psshBox, kids);

                for (int j = 0; j < mTracks.size(); j++) {
                    mTracks.get(j).getMediaFormat().setString(KEY_MARLIN_JSON, psshJson);
                }
            }

            byte[] rawPssh = new byte[psshData.length - 24];

            System.arraycopy(psshData, 24, rawPssh, 0, rawPssh.length);

            addMetaDataValue(KEY_DRM_UUID, systemId);

            addMetaDataValue(KEY_DRM_PSSH_DATA, rawPssh);

            return true;
        } catch (EOFException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error parsing pssh data", e);
            return false;
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error parsing pssh data", e);
            return false;
        } catch (JSONException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error parsing pssh data", e);
            return false;
        }
    }

    private boolean parseElst(BoxHeader header) {
        try {
            mDataSource.skipBytes(4); // version and flags
            int entryCount = mDataSource.readInt();
            for (int i = 0; i < entryCount; i++) {
                long segmentDurationTicks = 0;
                long mediaTimeTicks = 0;
                // if (((versionFlags >> 24) & 0xFF) == 1){
                // segmentDuration = mDataSource.readLong();
                // mediaTime = mDataSource.readLong();
                // } else {
                segmentDurationTicks = mDataSource.readInt();
                mediaTimeTicks = mDataSource.readInt();
                // }
                short mediaRateInteger = mDataSource.readShort();
                short mediaRateFraction = mDataSource.readShort();
                long segmentDurationUs = segmentDurationTicks * 1000000 / mFileTimescale;
                long mediaTimeUs = mediaTimeTicks * 1000000 / mFileTimescale;
                // TODO: Do something useful with this information
            }
        } catch (EOFException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error parsing 'elst' box", e);

            mCurrentBoxSequence.removeLast();
            return false;
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Error parsing 'elst' box", e);

            mCurrentBoxSequence.removeLast();
            return false;
        }
        return true;
    }

    private boolean parseTfra(BoxHeader header) {
        try {
            int versionFlags = mDataSource.readInt();
            int version = (versionFlags >> 24) & 0xFF;

            int trackId = mDataSource.readInt();
            int lengths = mDataSource.readInt();
            int lengthSizeOfTrafNum = ((lengths >> 4) & 0x00000003) + 1;
            int lengthSizeOfTrunNum = ((lengths >> 2) & 0x00000003) + 1;
            int lengthSizeOfSampleNum = (lengths & 0x00000003) + 1;
            int numberOfEntry = mDataSource.readInt();
            ArrayList<Tfra> tfraList = new ArrayList<Tfra>(numberOfEntry);
            for (int i = 0; i < numberOfEntry; i++) {
                long timeTicks = 0;
                long moofOffset = 0;
                if (version == 1) {
                    timeTicks = mDataSource.readLong();
                    moofOffset = mDataSource.readLong();
                } else {
                    timeTicks = mDataSource.readInt();
                    moofOffset = mDataSource.readInt();
                }
                byte[] lengthsData = new byte[lengthSizeOfTrafNum + lengthSizeOfTrunNum + lengthSizeOfSampleNum];
                mDataSource.read(lengthsData);
                long sampleNumber = 0;
                for (int j = lengthSizeOfTrafNum + lengthSizeOfTrunNum; j < lengthsData.length; j++) {
                    sampleNumber = sampleNumber << 8 & (lengthsData[j] & 0xFF);
                }
                Tfra tfra = new Tfra();
                tfra.timeTicks = timeTicks;
                tfra.moofOffset = moofOffset;
                tfra.sampleNumber = sampleNumber;
                tfraList.add(tfra);
            }
            IsoTrack track = null;
            int numTracks = mTracks.size();
            for (int i = 0; i < numTracks; i++) {
                IsoTrack t = (IsoTrack) (mTracks.get(i));
                if (t.getTrackId() == trackId) {
                    track = t;
                    break;
                }
            }
            if (track != null) {
                track.setTfraList(tfraList);
            } else {
                track = (IsoTrack) createTrack();
                track.setTrackId(trackId);
                track.setTfraList(tfraList);
                mMfraTracks.add(track);
            }

        } catch (EOFException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "EOFException while parsing 'tfra' box", e);
            return false;
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'tfra' box", e);
            return false;
        }
        return true;
    }

    private boolean parseTrun(BoxHeader header) {
        if (mCurrentMoofTrackId != mCurrentTrackId || mSkipInsertSamples) {
            return true;
        }
        try {
            int versionFlags = mDataSource.readInt();
            int sampleCount = mDataSource.readInt();
            int dataOffset = 0;
            ArrayList<FragmentSample> fragmentSamples = new ArrayList<FragmentSample>(sampleCount);
            if ((versionFlags & 0x000001) != 0) {
                dataOffset = mDataSource.readInt();
            }
            if ((versionFlags & 0x000004) != 0) {
                mDataSource.skipBytes(4);
            }
            long sumSampleSizes = 0;
            for (int i = 0; i < sampleCount; i++) {
                FragmentSample sample = new FragmentSample();
                if ((versionFlags & 0x000100) != 0) {
                    sample.durationTicks = mDataSource.readInt();
                } else if (mCurrentTrackFragment.defaultSampleDuration != Integer.MIN_VALUE) {
                    sample.durationTicks = mCurrentTrackFragment.defaultSampleDuration;
                } else {
                    Trex trex = mCurrentTrack.getTrex();
                    if (trex != null) {
                        sample.durationTicks = trex.defaultSampleDuration;
                    } else {
                        if (LOGS_ENABLED)
                            Log.e(TAG, "no applicable values for fragment sample duration available");

                        mCurrentBoxSequence.removeLast();
                        return false;
                    }
                }
                if ((versionFlags & 0x000200) != 0) {
                    sample.size = mDataSource.readInt();
                } else if (mCurrentTrackFragment.defaultSampleSize != Integer.MIN_VALUE) {
                    sample.size = mCurrentTrackFragment.defaultSampleSize;
                } else {
                    Trex trex = mCurrentTrack.getTrex();
                    if (trex != null) {
                        sample.size = trex.defaultSampleSize;
                    } else {
                        if (LOGS_ENABLED)
                            Log.e(TAG, "no applicable values for fragment sample size available");

                        mCurrentBoxSequence.removeLast();
                        return false;
                    }
                }
                if ((versionFlags & 0x000400) != 0) {
                    mDataSource.skipBytes(4);
                }
                if ((versionFlags & 0x000800) != 0) {
                    sample.compositionTimeOffset = mDataSource.readInt();
                }
                if ((versionFlags & 0x000001) != 0) {
                    sample.dataOffset = mCurrentTrackFragment.baseDataOffset + dataOffset + sumSampleSizes;
                } else {
                    sample.dataOffset = mCurrentTrackFragment.baseDataOffset + mPrevTrunDataSize + sumSampleSizes;
                }
                sumSampleSizes += sample.size;
                fragmentSamples.add(sample);
            }
            mPrevTrunDataSize += sumSampleSizes;
            mMoofDataSize += sumSampleSizes;
            mCurrentTrack.addFragmentSamples(fragmentSamples);
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'trun' box", e);
            mCurrentBoxSequence.removeLast();
            return false;
        }
        return true;
    }

    protected boolean parseTfhd(BoxHeader header) {
        try {
            int versionFlags = mDataSource.readInt();
            int trackId = mDataSource.readInt();
            mCurrentMoofTrackId = trackId;
            if (trackId == mCurrentTrackId || mCurrentTrackId == -1) {
                if (mSkipInsertSamples) {
                    mDesiredTrackIdFound = true;
                    return true;
                }
            } else {
                if (LOGS_ENABLED)
                    Log.i(TAG,
                            "parse tfhd for track " + trackId + ", but we're looking for track " + mCurrentTrackId);
                return true;
            }
            IsoTrack track = null;
            int numTracks = mTracks.size();
            for (int i = 0; i < numTracks; i++) {
                IsoTrack t = (IsoTrack) (mTracks.get(i));
                if (t.getTrackId() == trackId) {
                    track = t;
                    break;
                }
            }
            if (track != null) {
                mCurrentTrack = track;
            } else {
                if (LOGS_ENABLED)
                    Log.e(TAG, "track id " + trackId + " in current 'trun' box does not exist in list of tracks");

                mCurrentBoxSequence.removeLast();
                return false;
            }
            if ((versionFlags & 0x000001) != 0) {
                mCurrentTrackFragment.baseDataOffset = mDataSource.readLong();
            } else {
                mCurrentTrackFragment.baseDataOffset = mCurrentMoofOffset;
            }
            if ((versionFlags & 0x000002) != 0) {
                mDataSource.skipBytes(4);
            }
            if ((versionFlags & 0x000008) != 0) {
                mCurrentTrackFragment.defaultSampleDuration = mDataSource.readInt();
            }
            if ((versionFlags & 0x000010) != 0) {
                mCurrentTrackFragment.defaultSampleSize = mDataSource.readInt();
            }
            if ((versionFlags & 0x000020) != 0) {
                mDataSource.skipBytes(4);
            }

        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'tfhd' box", e);
            mCurrentBoxSequence.removeLast();
            return false;
        }
        return true;
    }

    private boolean parseHdlr(BoxHeader header) {
        byte[] data = new byte[(int) header.boxDataSize];
        try {
            if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                mCurrentBoxSequence.removeLast();
                return false;
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'hdlr' box", e);
            return false;
        }
        // skip 4 for version and flags
        // skip 4 for pre_defined
        int handlerType = (data[8] & 0xFF) << 24 | (data[9] & 0xFF) << 16 | (data[10] & 0xFF) << 8
                | data[11] & 0xFF;
        TrackType trackType = TrackType.UNKNOWN;
        if (handlerType == HANDLER_TYPE_AUDIO) {
            trackType = TrackType.AUDIO;
        } else if (handlerType == HANDLER_TYPE_VIDEO) {
            trackType = TrackType.VIDEO;
        } else if (handlerType == HANDLER_TYPE_HMMP_SUBTITLE || handlerType == HANDLER_TYPE_SUBTITLE) {
            trackType = TrackType.SUBTITLE;
        } else {
            if (LOGS_ENABLED && mIsParsingTrack)
                Log.d(TAG, "Track " + mCurrentTrack.getTrackId() + " is unknown");
        }
        // skip 12 for reserved
        // skip remaining bytes for name
        if (mIsParsingTrack) {
            mCurrentTrack.setTrackType(trackType);
            if (handlerType == HANDLER_TYPE_OBJECT_DESCRIPTOR) {
                mParseODSMData = true;
            } else {
                if (LOGS_ENABLED)
                    Log.w(TAG, "unknown handler type 0x" + Util.bytesToHex(data, 8, 4));
            }
        } else {
            if (LOGS_ENABLED)
                Log.w(TAG, "Not parsing track, this is meta hdlr box");
        }
        return true;
    }

    private boolean parseMvhd(BoxHeader header) {
        int versionFlags = 0;
        try {
            versionFlags = mDataSource.readInt();
            int version = versionFlags >> 24;
            // long creationTime = 0;
            // long modificationTime = 0;
            long durationTicks = 0;
            if (version == 1) {
                // creationTime = mDataSource.readLong();
                // modificationTime = mDataSource.readLong();
                mDataSource.skipBytes(16);
                mFileTimescale = mDataSource.readInt();
                durationTicks = mDataSource.readLong();
            } else {
                // creationTime = mDataSource.readInt();
                // modificationTime = mDataSource.readInt();
                mDataSource.skipBytes(8);
                mFileTimescale = mDataSource.readInt();
                durationTicks = mDataSource.readInt();
            }
            addMetaDataValue(KEY_DURATION, durationTicks * 1000 / mFileTimescale);
            // Skip rest of box
            int skipCount = 4 + 2 + 2 + 8 + 36 + 24 + 4;
            mDataSource.skipBytes(skipCount);
        } catch (EOFException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "EOFException while parsing 'mvhd' box", e);
            return false;
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'mvhd' box", e);
            return false;
        }
        return true;
    }

    private boolean parseMdhd(BoxHeader header) {
        byte[] data = new byte[(int) header.boxDataSize];
        try {
            if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                mCurrentBoxSequence.removeLast();
                return false;
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'mdhd' box", e);
        }
        int version = data[0];
        int timescale = 0;
        long durationTicks = 0;
        short lang;
        String language;
        if (version == 1) {
            // skip 8 for creation_time
            // skip 8 for modification_time
            timescale = (data[20] & 0xFF) << 24 | (data[21] & 0xFF) << 16 | (data[22] & 0xFF) << 8
                    | data[23] & 0xFF;
            durationTicks = (long) (data[24] & 0xFF) << 56 | (long) (data[25] & 0xFF) << 48
                    | (long) (data[26] & 0xFF) << 40 | (long) (data[27] & 0xFF) << 32 | (data[28] & 0xFF) << 24
                    | (data[29] & 0xFF) << 16 | (data[30] & 0xFF) << 8 | data[31] & 0xFF;
            lang = (short) ((data[32] & 0xFF) << 8 | data[33] & 0xFF);
        } else { // version 0
            // skip 4 for creation_time
            // skip 4 for modification_time
            timescale = (data[12] & 0xFF) << 24 | (data[13] & 0xFF) << 16 | (data[14] & 0xFF) << 8
                    | data[15] & 0xFF;
            durationTicks = (data[16] & 0xFF) << 24 | (data[17] & 0xFF) << 16 | (data[18] & 0xFF) << 8
                    | data[19] & 0xFF;
            lang = (short) ((data[20] & 0xFF) << 8 | data[21] & 0xFF);
        }
        char c1 = (char) ((lang >> 10) + 0x60);
        char c2 = (char) (((lang >> 5) & 31) + 0x60);
        char c3 = (char) ((lang & 31) + 0x60);
        language = "" + c1 + c2 + c3;
        mCurrentTrack.setLanguage(language);
        mCurrentTrack.getMetaData().addValue(KEY_LANGUAGE, language);
        mCurrentTrack.setTimeScale(timescale);
        long trackDurationMs = durationTicks * 1000 / timescale;
        mCurrentTrack.getMetaData().addValue(KEY_DURATION, trackDurationMs);
        return true;
    }

    private boolean readTkhd(BoxHeader header) {
        byte[] data = new byte[(int) header.boxDataSize];
        try {
            if (mDataSource.readAt(mCurrentOffset, data, data.length) != data.length) {
                mCurrentBoxSequence.removeLast();
                return false;
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException while parsing 'tkhd' box", e);
            mCurrentBoxSequence.removeLast();
            return false;
        }

        return parseTkhd(data);
    }

    protected boolean parseTkhd(byte[] data) {
        int trackId = 0;
        // long duration = 0;
        boolean trackEnabled = false;
        int flags = (data[1] & 0xFF) << 16 | (data[2] & 0xFF) << 8 | data[3] & 0xFF;
        if (data[0] == 1) { // version 1
            // skip 8 for creation_time
            // skip 8 for modification_time
            trackId = (data[20] & 0xFF) << 24 | (data[21] & 0xFF) << 16 | (data[22] & 0xFF) << 8 | data[23] & 0xFF;
            // skip 4 for reserved
            // duration = (data[28] & 0xFF) << 56 | (data[29] & 0xFF) << 48 |
            // (data[30] & 0xFF) << 40 | (data[31] & 0xFF) << 32 |
            // (data[32] & 0xFF) << 24 | (data[33] & 0xFF) << 16 |
            // (data[34] & 0xFF) << 8 | data[35] & 0xFF;
        } else { // version 0
                 // skip 4 for creation_time
                 // skip 4 for modification_time
            trackId = (data[12] & 0xFF) << 24 | (data[13] & 0xFF) << 16 | (data[14] & 0xFF) << 8 | data[15] & 0xFF;
            // skip 4 for reserved
            // duration = (data[20] & 0xFF) << 24 | (data[21] & 0xFF) << 16 |
            // (data[22] & 0xFF) << 8 | data[23] & 0xFF;
        }
        if ((flags & 1) == 1) {
            trackEnabled = true;
        } else {
            if (LOGS_ENABLED)
                Log.w(TAG, "track not enabled");
        }

        boolean foundTrack = false;
        for (Track track : mTracks) {
            if (((IsoTrack) track).getTrackId() == trackId) {
                mCurrentTrack = (IsoTrack) track;
                foundTrack = true;
            }
        }

        if (!foundTrack) {
            mCurrentTrack = (IsoTrack) createTrack();
            mCurrentTrack.setTrackId(trackId);
            mTracks.add(mCurrentTrack);
        }

        return true;
    }

    protected void parseVisualSampleEntry(byte[] data) {
        /*
         * VisualSampleEntry extends SampleEntry in ISO/IEC 14496 part 12, see
         * section 8.5.2.2 for structure
         */
        mCurrentOffset += data.length;
        // SampleEntry
        // skip 6 for reserved
        // skip 2 for data_reference_index
        // VisualSampleEntry
        // skip 2 for pre_defined
        // skip 2 for reserved
        // skip 12 for pre_defined
        int width = ((data[24] & 0xFF) << 8 | data[25] & 0xFF) & 0x0000FFFF;
        int height = ((data[26] & 0xFF) << 8 | data[27] & 0xFF) & 0x0000FFFF;
        // skip 4 for horizresolution
        // skip 4 for vertresolution
        // skip 4 for reserved
        // skip 2 for frame_count
        // skip 32 for compressorname
        // skip 2 for depth
        // skip 2 for pre_defined
        mCurrentMediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
        mCurrentTrack.getMetaData().addValue(KEY_WIDTH, width);
        mCurrentMediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
        mCurrentTrack.getMetaData().addValue(KEY_HEIGHT, height);
        int maxSize = ((width + 15) / 16) * ((height + 15) / 16) * 192;
        mCurrentMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxSize);
        addMetaDataValue(KEY_WIDTH, width);
        addMetaDataValue(KEY_HEIGHT, height);
    }

    protected void parseAudioSampleEntry(byte[] data) {
        mCurrentOffset += data.length;
        // skip 6 for reserved
        // skip 2 for data_reference_index
        // skip 8 for reserved
        int channelCount = ((data[16] & 0xFF) << 8 | data[17] & 0xFF) & 0x0000FFFF;
        // skip 2 for samplesize
        // skip 2 for pre_defined
        // skip 2 for reserved
        int sampleRate = ((data[24] & 0xFF) << 8 | data[25] & 0xFF) & 0x0000FFFF;
        // skip 2 for sampleRate lower
        mCurrentMediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelCount);
        mCurrentTrack.getMetaData().addValue(MetaData.KEY_CHANNEL_COUNT, channelCount);
        mCurrentMediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate);
        int maxSize = 8192 * 4; // aac max size
        mCurrentMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxSize);
    }

    protected AvccData parseAvcc(byte[] buffer) {
        AvccData avccData = new AvccData();
        avccData.spsBuffer = ByteBuffer.allocate(1024);
        avccData.ppsBuffer = ByteBuffer.allocate(1024);
        int currentBufferOffset = 0;
        if (buffer[0] != 1) { // configurationVersion
            return null;
        }

        mNALLengthSize = (buffer[4] & 3) + 1; // lengthSizeMinusOne;
        int numSPS = buffer[5] & 31; // numOfSequenceParameterSets
        currentBufferOffset = 6;
        for (int i = 0; i < numSPS; i++) {
            int spsLength = (((buffer[currentBufferOffset++] & 0xFF) << 8) | buffer[currentBufferOffset++] & 0xFF)
                    & 0x0000FFFF;
            byte[] spsUnit = new byte[spsLength + 4];
            spsUnit[0] = 0;
            spsUnit[1] = 0;
            spsUnit[2] = 0;
            spsUnit[3] = 1;
            for (int j = 0; j < spsLength; j++) {
                spsUnit[j + 4] = buffer[currentBufferOffset + j];
            }
            avccData.spsBuffer.put(spsUnit);
            currentBufferOffset += spsLength;
        }
        int numPPS = buffer[currentBufferOffset++]; // numOfPictureParameterSets
        for (int i = 0; i < numPPS; i++) {
            int ppsLength = ((buffer[currentBufferOffset++] & 0xFF) << 8 | buffer[currentBufferOffset++] & 0xFF)
                    & 0x0000FFFF;
            byte[] ppsUnit = new byte[ppsLength + 4];
            ppsUnit[0] = 0;
            ppsUnit[1] = 0;
            ppsUnit[2] = 0;
            ppsUnit[3] = 1;
            for (int j = 0; j < ppsLength; j++) {
                ppsUnit[j + 4] = buffer[currentBufferOffset + j];
            }
            avccData.ppsBuffer.put(ppsUnit);
            currentBufferOffset += ppsLength;
        }
        return avccData;
    }

    protected byte[] parseHvcc(byte[] buffer) {
        byte[] hvccData = null;
        if (buffer[0] != 1) {
            if (LOGS_ENABLED)
                Log.e(TAG, "HVCC Version " + buffer[0] + " not supported");
            return null;
        }
        mNALLengthSize = (buffer[21] & 3) + 1; // lengthSizeMinusOne;
        int numArrays = buffer[22];
        int currentBufferOffset = 23;
        for (int i = 0; i < numArrays; i++) {
            currentBufferOffset++; // skip arrayedCompleteness, reserved,
                                   // nalUnitType
            int numNalUnits = ((buffer[currentBufferOffset++] & 0xFF) << 8 | buffer[currentBufferOffset++] & 0xFF)
                    & 0x0000FFFF;
            for (int j = 0; j < numNalUnits; j++) {
                int nalUnitLength = ((buffer[currentBufferOffset++] & 0xFF) << 8
                        | buffer[currentBufferOffset++] & 0xFF) & 0x0000FFFF;
                if (hvccData == null) {
                    hvccData = new byte[nalUnitLength + 4];
                    hvccData[0] = 0x00;
                    hvccData[1] = 0x00;
                    hvccData[2] = 0x00;
                    hvccData[3] = 0x01;
                    System.arraycopy(buffer, currentBufferOffset, hvccData, 4, nalUnitLength);
                } else {
                    byte[] newArray = new byte[hvccData.length + nalUnitLength + 4];
                    System.arraycopy(hvccData, 0, newArray, 0, hvccData.length);
                    newArray[hvccData.length] = 0x00;
                    newArray[hvccData.length + 1] = 0x00;
                    newArray[hvccData.length + 2] = 0x00;
                    newArray[hvccData.length + 3] = 0x01;
                    System.arraycopy(buffer, currentBufferOffset, newArray, hvccData.length + 4, nalUnitLength);
                    hvccData = newArray;
                }
                currentBufferOffset += nalUnitLength;
            }
        }

        return hvccData;
    }

    private boolean parseESDS(byte[] esds) {
        if (esds[0] != 0x03) {
            if (LOGS_ENABLED)
                Log.e(TAG, "ESDS wrong tag, expected 0x03");
            return false;
        }

        int offset = 1;
        int dataSize = 0;
        boolean more;
        do {
            int x = esds[offset++];

            dataSize = (dataSize << 7) | (x & 0x7f);
            more = (x & 0x80) != 0;
        } while (more);

        if (offset + dataSize > esds.length) {
            return false;
        }

        offset += 2;

        boolean streamDependenceFlag = (esds[offset] & 0x80) > 0;
        boolean URL_Flag = (esds[offset] & 0x40) > 0;
        boolean OCRstreamFlag = (esds[offset] & 0x20) > 0;

        offset++;

        if (streamDependenceFlag) {
            offset += 2;
        }

        if (URL_Flag) {
            int URLlength = esds[offset++];

            offset += URLlength;
        }

        if (OCRstreamFlag) {
            offset += 2;
        }

        if (esds[offset++] != 0x04) {
            if (LOGS_ENABLED)
                Log.e(TAG, "ESDS wrong tag, expected 0x04");
            return false;
        }

        dataSize = 0;
        do {
            int x = esds[offset++];
            dataSize = (dataSize << 7) | (x & 0x7f);
            more = (x & 0x80) != 0;
        } while (more);

        if (offset + dataSize > esds.length) {
            if (LOGS_ENABLED)
                Log.e(TAG, "buffer too small");
            return false;
        }

        offset += 13;

        if (esds[offset++] != 0x05) {
            if (LOGS_ENABLED)
                Log.e(TAG, "wrong tag expected 0x05");
            return false;
        }

        dataSize = 0;
        do {
            int x = esds[offset++];
            dataSize = (dataSize << 7) | (x & 0x7f);
            more = (x & 0x80) != 0;
        } while (more);

        if (offset + dataSize > esds.length) {
            return false;
        }

        mCurrentMediaFormat.setByteBuffer("csd-0",
                ByteBuffer.wrap(Arrays.copyOfRange(esds, offset, offset + dataSize)));
        return true;
    }

    private boolean parseDataBox(BoxHeader header) {
        try {
            if (mCurrentMetaDataKey != null) {
                String metaDataValue;
                if (mCurrentMetaDataKey == KEY_DISC_NUMBER) {
                    mDataSource.skipBytes(4); // skip type
                    mDataSource.skipBytes(4); // skip locale
                    mDataSource.skipBytes(2); // skip album id type
                                              // and album id len
                    int diskNumber = mDataSource.readShort();
                    mDataSource.skipBytes(2); // skip total number of disks
                    metaDataValue = Integer.toString(diskNumber);
                } else {
                    byte[] typefield = new byte[4];
                    mDataSource.read(typefield);
                    int type = typefield[0];
                    int flag = (typefield[1] | typefield[2] | typefield[3]);

                    Charset encoding = StandardCharsets.UTF_8;
                    if (type == 2) {
                        encoding = StandardCharsets.UTF_16BE;
                    }
                    mDataSource.skipBytes(4); // skip locale for now
                    byte[] data = new byte[(int) header.boxDataSize - 8];
                    mDataSource.read(data);
                    if (mCurrentMetaDataKey == KEY_TRACK_NUMBER) {
                        int trackNumber = (data[2] << 8) & 0xFF | data[3];
                        int trackTotalNumber = (data[4] << 8) & 0xFF | data[5];
                        metaDataValue = trackNumber + "/" + trackTotalNumber;
                    } else if (mCurrentMetaDataKey == KEY_COMPILATION) {
                        metaDataValue = Byte.toString(data[0]);
                    } else if (mCurrentMetaDataKey == KEY_GENRE) {
                        if (type == 0 && flag == 1) {
                            metaDataValue = new String(data, encoding);
                        } else {
                            int genre = data[data.length - 1];
                            genre--;
                            if (genre < 0) {
                                genre = 255;
                            }
                            metaDataValue = Integer.toString(genre);
                        }
                    } else {
                        metaDataValue = new String(data, encoding);
                    }
                }
                mMetaDataValues.put(mCurrentMetaDataKey, metaDataValue);
            }
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "could not read data", e);

            return false;
        }
        return true;
    }

    protected static class BoxHeader {
        public long startOffset = 0;

        public long boxDataSize = 0;

        public int boxHeaderSize = 0;

        public int boxType = 0;
    }

    protected static class AvccData {
        ByteBuffer spsBuffer;

        ByteBuffer ppsBuffer;
    }

    public class IsoTrack implements Track {
        protected MetaDataImpl mMetaData;

        protected SampleTable mSampleTable;

        protected int mTimeScale = 0;

        protected int mTrackId = -1;

        protected int mTrackIndex = -1;

        protected MediaFormat mMediaFormat;

        protected TrackType mType;

        protected int mCurrentSampleIndex = 0;

        protected Trex mTrex;

        protected String mLanguage;

        protected ArrayList<Tfra> mTfraList;

        protected ArrayDeque<FragmentSample> mCurrentFragmentSampleQueue;

        protected ArrayDeque<CryptoInfo> mCurrentCryptoInfoQueue;

        public long mTimeTicks = 0;

        protected byte[] mKID;

        protected long mEditMediaTimeTicks = 0;

        protected int mDefaultIVSize = 0;

        protected byte[] mDefaultKID;

        protected int mCurrentSampleDescriptionIndex = -1;

        protected ArrayList<MediaFormat> mSampleDescriptionList;

        protected long mNextMoofOffset = 0;

        protected long mLastTimestampUs = 0;

        public IsoTrack() {
            mMetaData = new MetaDataImpl();
            mSampleTable = new SampleTable();
            mCurrentFragmentSampleQueue = null;
            mSampleDescriptionList = new ArrayList<MediaFormat>(1);
        }

        public boolean buildSampleTable() {
            return mSampleTable.buildSampleTable();
        }

        public void releaseSampleTable() {
            mSampleTable.releaseSampleTable();
        }

        public void addSampleDescriptionEntry(MediaFormat mediaFormat) {
            if (mMediaFormat == null) {
                mMediaFormat = mediaFormat;
            }
            mSampleDescriptionList.add(mediaFormat);
        }

        public ArrayList<Tfra> getTfraList() {
            return mTfraList;
        }

        public void addFragmentSamples(ArrayList<FragmentSample> fragmentSamples) {
            int numNewSamples = fragmentSamples.size();
            if (mCurrentFragmentSampleQueue == null) {
                mCurrentFragmentSampleQueue = new ArrayDeque<FragmentSample>(numNewSamples);
            }
            for (int i = 0; i < numNewSamples; i++) {
                mCurrentFragmentSampleQueue.add(fragmentSamples.get(i));
            }
        }

        public void addCryptoInfos(ArrayList<CryptoInfo> cryptoInfos) {
            int numNewSamples = cryptoInfos.size();
            if (mCurrentCryptoInfoQueue == null) {
                mCurrentCryptoInfoQueue = new ArrayDeque<CryptoInfo>(numNewSamples);
            }
            for (int i = 0; i < numNewSamples; i++) {
                mCurrentCryptoInfoQueue.add(cryptoInfos.get(i));
            }
        }

        public void setTfraList(ArrayList<Tfra> tfraEntryList) {
            mTfraList = tfraEntryList;
        }

        public AccessUnit dequeueAccessUnit(boolean readFragmented) {
            /*
             * if (LOGS_ENABLED) Log.v(TAG, "dequeueAccessUnit track " +
             * mTrackId + " sample " + mCurrentSampleIndex);
             */

            if (readFragmented && mCurrentSampleIndex >= mSampleTable.getSampleCount()) {
                return dequeueAccessUnitFragmented();
            }
            AccessUnit accessUnit = new AccessUnit();

            if (mCurrentSampleIndex >= mSampleTable.getSampleCount()) {
                accessUnit.status = AccessUnit.END_OF_STREAM;
                return accessUnit;
            }
            if (mCurrentSampleIndex >= mSampleTable.getSampleCount()) {
                accessUnit.status = AccessUnit.ERROR;
                return accessUnit;
            }
            int sampleDescriptionIndex = mSampleTable.getSampleDescriptionIndex(mCurrentSampleIndex);
            if (mCurrentSampleDescriptionIndex == -1) {
                mCurrentSampleDescriptionIndex = sampleDescriptionIndex;
            } else if (sampleDescriptionIndex != mCurrentSampleDescriptionIndex) {
                mCurrentSampleDescriptionIndex = sampleDescriptionIndex;
                MediaFormat mediaFormat = mSampleDescriptionList.get(mCurrentSampleDescriptionIndex);
                // TODO Send new format to codec
            }
            accessUnit.status = AccessUnit.OK;
            accessUnit.trackIndex = mTrackIndex;
            accessUnit.timeUs = mSampleTable.getTimestampUs(mCurrentSampleIndex)
                    - mEditMediaTimeTicks * 1000000 / mTimeScale;
            accessUnit.durationUs = mSampleTable.getDurationUs(mCurrentSampleIndex);
            long dataOffset = mSampleTable.getOffset(mCurrentSampleIndex);
            int dataSize = mSampleTable.getSize(mCurrentSampleIndex);
            if (accessUnit.data == null || accessUnit.data.length < dataSize) {
                accessUnit.data = null;
                accessUnit.data = new byte[dataSize];
            }
            accessUnit.size = dataSize;
            try {
                if (mDataSource.readAt(dataOffset, accessUnit.data, dataSize) != dataSize) {
                    accessUnit.status = AccessUnit.ERROR;
                    return accessUnit;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while reading accessunit from source");
                accessUnit.status = AccessUnit.ERROR;
                return accessUnit;
            }
            if (mMediaFormat.getString(MediaFormat.KEY_MIME).equals(MimeType.AVC)
                    || mMediaFormat.getString(MediaFormat.KEY_MIME).equals(MimeType.HEVC)) {
                // add NAL Header
                int srcOffset = 0;
                int dstOffset = 0;
                int nalLengthSize = 4;
                // TODO: Support files with nalLengthSize other than 4
                while (srcOffset < dataSize) {
                    if ((srcOffset + nalLengthSize) > dataSize) {
                        accessUnit.status = AccessUnit.ERROR;
                        return accessUnit;
                    }
                    int nalLength = (accessUnit.data[srcOffset++] & 0xff) << 24
                            | (accessUnit.data[srcOffset++] & 0xff) << 16
                            | (accessUnit.data[srcOffset++] & 0xff) << 8 | (accessUnit.data[srcOffset++] & 0xff);
                    if (srcOffset + nalLength > dataSize) {
                        accessUnit.status = AccessUnit.ERROR;
                        return accessUnit;
                    }
                    accessUnit.data[dstOffset++] = 0;
                    accessUnit.data[dstOffset++] = 0;
                    accessUnit.data[dstOffset++] = 0;
                    accessUnit.data[dstOffset++] = 1;
                    srcOffset += nalLength;
                    dstOffset += nalLength;
                }
            }
            accessUnit.isSyncSample = mSampleTable.isSyncSample(mCurrentSampleIndex);

            mLastTimestampUs = accessUnit.timeUs;

            mCurrentSampleIndex++;
            return accessUnit;
        }

        protected AccessUnit dequeueAccessUnitFragmented() {
            // if (LOGS_ENABLED) Log.v(TAG, "dequeueAccessUnitFragmented track "
            // + mTrackId);

            AccessUnit accessUnit = new AccessUnit();

            // load moof box when necessary

            if (!fillFragmentQueue()) {
                accessUnit.status = AccessUnit.ERROR;
                return accessUnit;
            }

            if (mCurrentFragmentSampleQueue == null || mCurrentFragmentSampleQueue.isEmpty()) {
                if (LOGS_ENABLED)
                    Log.i(TAG, "No more fragments in queue, end of stream");
                accessUnit.status = AccessUnit.END_OF_STREAM;
                return accessUnit;
            }
            FragmentSample sample = mCurrentFragmentSampleQueue.removeFirst();
            accessUnit.status = AccessUnit.OK;
            accessUnit.timeUs = (mTimeTicks + sample.compositionTimeOffset - mEditMediaTimeTicks) * 1000000
                    / mTimeScale;
            accessUnit.timeUs += mSampleTable.getDurationUs();

            if (accessUnit.timeUs < 0) {
                if (LOGS_ENABLED)
                    Log.w(TAG, "Negative sampletime!");
                accessUnit.timeUs = 0;
            }
            accessUnit.durationUs = (long) sample.durationTicks * 1000000L / mTimeScale;
            mTimeTicks += sample.durationTicks;
            long dataOffset = sample.dataOffset;
            int dataSize = sample.size;
            if (accessUnit.data == null || accessUnit.data.length < dataSize) {
                accessUnit.data = null;
                accessUnit.data = new byte[dataSize];
            }
            accessUnit.size = dataSize;
            try {
                if (mDataSource.readAt(dataOffset, accessUnit.data, dataSize) != dataSize) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "could not read sample data");
                    accessUnit.status = AccessUnit.ERROR;
                    return accessUnit;
                }
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException while reading accessunit from source");
                accessUnit.status = AccessUnit.ERROR;
                return accessUnit;
            }

            if (mCurrentCryptoInfoQueue != null) {
                accessUnit.cryptoInfo = mCurrentCryptoInfoQueue.removeFirst();

                if (accessUnit.cryptoInfo != null && accessUnit.cryptoInfo.numSubSamples == 1
                        && accessUnit.cryptoInfo.numBytesOfEncryptedData[0] == -1) {
                    accessUnit.cryptoInfo.numBytesOfEncryptedData[0] = dataSize;
                }
            }

            boolean isAVC = mMediaFormat.getString(MediaFormat.KEY_MIME).equals(MimeType.AVC);
            boolean isHEVC = mMediaFormat.getString(MediaFormat.KEY_MIME).equals(MimeType.HEVC);

            // Add NAL header. If we have no clearbytes, we need to
            // let the platform set NAL header
            if ((isAVC || isHEVC)
                    && (accessUnit.cryptoInfo == null || accessUnit.cryptoInfo.numBytesOfClearData[0] > 0)) {
                int srcOffset = 0;
                int dstOffset = 0;
                while (srcOffset < dataSize) {
                    if ((srcOffset + mNALLengthSize) > dataSize) {
                        if (LOGS_ENABLED)
                            Log.e(TAG, "no room to add nal length");
                        accessUnit.status = AccessUnit.ERROR;
                        return accessUnit;
                    }
                    int nalLength = 0;

                    if (mNALLengthSize == 1 || mNALLengthSize == 2) {
                        if (mNALLengthSize == 1) {
                            nalLength = accessUnit.data[srcOffset];
                        } else if (mNALLengthSize == 2) {
                            nalLength = ((accessUnit.data[srcOffset] & 0xff) << 8
                                    | (accessUnit.data[srcOffset + 1] & 0xff));
                        }
                        byte[] tmpData = new byte[dataSize + (sNALHeaderSize - mNALLengthSize)];
                        if (srcOffset > 0) {
                            System.arraycopy(accessUnit.data, 0, tmpData, 0, srcOffset);
                        }
                        System.arraycopy(accessUnit.data, srcOffset, tmpData,
                                (sNALHeaderSize - mNALLengthSize) + srcOffset, accessUnit.data.length - srcOffset);
                        accessUnit.data = tmpData;
                        dataSize = accessUnit.data.length;
                        accessUnit.size = dataSize;
                    } else if (mNALLengthSize == 4) {
                        nalLength = ((accessUnit.data[srcOffset] & 0xff) << 24
                                | (accessUnit.data[srcOffset + 1] & 0xff) << 16
                                | (accessUnit.data[srcOffset + 2] & 0xff) << 8
                                | (accessUnit.data[srcOffset + 3] & 0xff));
                    } else {
                        if (LOGS_ENABLED)
                            Log.e(TAG, "unsupported nal length size" + mNALLengthSize);
                        accessUnit.status = AccessUnit.ERROR;
                        return accessUnit;
                    }
                    srcOffset += sNALHeaderSize;
                    if (accessUnit.cryptoInfo != null) {
                        accessUnit.cryptoInfo.numBytesOfClearData[0] += (sNALHeaderSize - mNALLengthSize);
                    }
                    if (srcOffset + nalLength > dataSize) {
                        if (LOGS_ENABLED)
                            Log.e(TAG, "error writing nal length");
                        accessUnit.status = AccessUnit.ERROR;
                        return accessUnit;
                    }
                    accessUnit.data[dstOffset++] = 0;
                    accessUnit.data[dstOffset++] = 0;
                    accessUnit.data[dstOffset++] = 0;
                    accessUnit.data[dstOffset++] = 1;

                    if (isAVC && (accessUnit.data[srcOffset] & 0x1f) == AVC_NAL_UNIT_TYPE_IDR_PICTURE) {
                        accessUnit.isSyncSample = true;
                    } else if (isHEVC) {
                        int nalType = (accessUnit.data[srcOffset] & 0x7e) >> 1;

                        if (nalType == HEVC_NAL_UNIT_TYPE_IDR_PICTURE_W_RADL
                                || nalType == HEVC_NAL_UNIT_TYPE_IDR_PICTURE_N_LP
                                || nalType == HEVC_NAL_UNIT_TYPE_CRA_PICTURE) {
                            accessUnit.isSyncSample = true;
                        }
                    }

                    srcOffset += nalLength;
                    dstOffset += nalLength;
                }
            } else if ((isAVC || isHEVC) && accessUnit.cryptoInfo != null
                    && accessUnit.cryptoInfo.numBytesOfClearData[0] == 0) {
                // nalType is encrypted, so we assume it is as sync sample
                accessUnit.isSyncSample = true;
            }
            mLastTimestampUs = accessUnit.timeUs;

            return accessUnit;
        }

        public void setTrackType(TrackType trackType) {
            mType = trackType;
        }

        @Override
        public TrackType getTrackType() {
            return mType;
        }

        public long getDurationUs() {
            if (mIsFragmented && mTfraList != null && mTimeScale > 0) {
                Tfra lastTfra = mTfraList.get(mTfraList.size() - 1);
                return lastTfra.timeTicks * 1000000 / mTimeScale;
            }
            return mSampleTable.getDurationUs();
        }

        public void setLanguage(String language) {
            mLanguage = language;
        }

        public String getLanguage() {
            return mLanguage;
        }

        public void setTrackId(int id) {
            mTrackId = id;
        }

        public int getTrackId() {
            return mTrackId;
        }

        public void setTrackIndex(int index) {
            mTrackIndex = index;
        }

        public int getTrackIndex() {
            return mTrackIndex;
        }

        @Override
        public MetaDataImpl getMetaData() {
            return mMetaData;
        }

        public SampleTable getSampleTable() {
            return mSampleTable;
        }

        public void setTimeScale(int timeScale) {
            mTimeScale = timeScale;
        }

        public int getTimeScale() {
            return mTimeScale;
        }

        public void setMediaFormat(MediaFormat format) {
            mMediaFormat = format;
        }

        @Override
        public MediaFormat getMediaFormat() {
            return mMediaFormat;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder(32);
            sb.append("TrackID = " + mTrackId + ",");
            // sb.append("Track type = '"+ccruof(mType)+"'");
            return sb.toString();
        }

        public long seekTo(long seekTimeUs, boolean isFragmented) {
            if (mCurrentCryptoInfoQueue != null) {
                mCurrentCryptoInfoQueue.clear();
            }
            if (!isFragmented || (mSampleTable != null && seekTimeUs < mSampleTable.getDurationUs())) {
                if (mSampleTable == null) {
                    return 0;
                }
                mCurrentSampleIndex = mSampleTable.findSampleIndex(seekTimeUs);
                // Fix until findSampleIndex can not return negative values
                if (mCurrentSampleIndex < 0) {
                    mCurrentSampleIndex = 0;
                }
                if (isFragmented) {
                    // Need to reset next moof offset
                    Tfra tfra = mTfraList.get(0);
                    mNextMoofOffset = tfra.moofOffset;
                    if (mCurrentFragmentSampleQueue != null) {
                        mCurrentFragmentSampleQueue.clear();
                    }
                    mTimeTicks = 0;
                }
                return mSampleTable.getTimeOfSample(mCurrentSampleIndex);
            } else {
                if (mTfraList == null) {
                    mCurrentFragmentSampleQueue = null;
                    mNextMoofOffset = 0;
                    mTimeTicks = 0;
                    return 0;
                }

                // need to set this so we actually get into
                // dequeueAccessUnitFragmented
                mCurrentSampleIndex = mSampleTable.getSampleCount();

                int numTfra = mTfraList.size();
                Tfra tfra = mTfraList.get(0);
                for (int i = 0; i < numTfra; i++) {
                    tfra = mTfraList.get(i);
                    long tfratimeUs = (tfra.timeTicks * 1000000 / mTimeScale);
                    if (tfratimeUs == seekTimeUs) {
                        break;
                    }
                    if (tfratimeUs > seekTimeUs) {
                        if (i > 0) {
                            tfra = mTfraList.get(i - 1);
                        }
                        break;
                    }
                }

                if (LOGS_ENABLED) {
                    Log.v(TAG, "Seek fragmented file track " + mType + " to "
                            + (tfra.timeTicks * 1000000 / mTimeScale) + " at offset " + tfra.moofOffset);
                }

                if (mCurrentFragmentSampleQueue != null) {
                    mCurrentFragmentSampleQueue.clear();
                }

                long contentLength = 0;
                try {
                    contentLength = mDataSource.length();
                } catch (IOException e) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "IOException when retrieving content length", e);
                }

                if (tfra.moofOffset > 0 && (tfra.moofOffset < contentLength || contentLength == -1)) {
                    mCurrentOffset = tfra.moofOffset;
                    BoxHeader header = getNextBoxHeader();
                    mCurrentTrackId = mTrackId;
                    if (header != null) {
                        if (header.boxType == BOX_ID_MOOF && parseBox(header)) {
                            // skip mdat to get next moof offset
                            mCurrentOffset = header.startOffset + header.boxHeaderSize + header.boxDataSize;
                            mNextMoofOffset = findNextMoofForTrack(mTrackId);
                        }
                    } else {
                        return 0;
                    }
                }

                mTimeTicks = tfra.timeTicks + mEditMediaTimeTicks
                        - (mSampleTable.getDurationUs() * mTimeScale / 1000000);

                if (mCurrentFragmentSampleQueue != null) {
                    long sampleNumber = tfra.sampleNumber;
                    if (mCurrentFragmentSampleQueue.size() >= sampleNumber) {
                        while (sampleNumber > 1) {
                            mCurrentFragmentSampleQueue.removeFirst();
                            sampleNumber--;
                        }
                        FragmentSample sample = mCurrentFragmentSampleQueue.peekFirst();
                        if (sample != null) {
                            mTimeTicks -= sample.compositionTimeOffset;
                        }
                    }

                    if (mType != TrackType.VIDEO) {
                        while (!mCurrentFragmentSampleQueue.isEmpty()) {
                            FragmentSample sample = mCurrentFragmentSampleQueue.peekFirst();
                            if (seekTimeUs * mTimeScale / 1000000L > mTimeTicks + sample.compositionTimeOffset
                                    - mEditMediaTimeTicks + sample.durationTicks) {
                                mTimeTicks += sample.durationTicks;
                                mCurrentFragmentSampleQueue.removeFirst();
                                if (mCurrentCryptoInfoQueue != null) {
                                    mCurrentCryptoInfoQueue.removeFirst();
                                }
                            } else {
                                break;
                            }
                        }
                    }
                }

                return (tfra.timeTicks * 1000000 / mTimeScale);
            }
        }

        public void setTrex(Trex t) {
            mTrex = t;
        }

        public Trex getTrex() {
            return mTrex;
        }

        public void setDefaultEncryptionData(int defaultIVSize, byte[] kID) {
            mDefaultIVSize = defaultIVSize;
            mDefaultKID = new byte[kID.length];
            mDefaultKID = Arrays.copyOf(kID, kID.length);

            mKID = new byte[kID.length];
            mKID = Arrays.copyOf(kID, kID.length);

        }

        public void setEditMediaTimeTicks(long mediaTimeTicks) {
            mEditMediaTimeTicks = mediaTimeTicks;
        }

        public long getLastTimestampUs() {
            return mLastTimestampUs;
        }

        public boolean hasDataAvailable(boolean isFragmented) throws IOException {
            if (isFragmented && mCurrentSampleIndex >= mSampleTable.getSampleCount()) {
                if (!fillFragmentQueue()) {
                    return false;
                }

                if (mCurrentFragmentSampleQueue == null) {
                    return false;
                }

                if (mCurrentFragmentSampleQueue.isEmpty()) {
                    if (mNextMoofOffset < 0) {
                        // End of stream, return true so other tracks can run to
                        // completion
                        return true;
                    } else {
                        return false;
                    }
                }

                FragmentSample sample = mCurrentFragmentSampleQueue.peek();
                DataAvailability hasData = mDataSource.hasDataAvailable(sample.dataOffset, sample.size);
                if (hasData == DataAvailability.NOT_AVAILABLE) {
                    mDataSource.seek(sample.dataOffset);
                }
                return hasData != DataAvailability.IN_FUTURE;
            } else {
                if (mCurrentSampleIndex >= mSampleTable.getSampleCount()) {
                    // End of stream, return true so other tracks can run to
                    // completion
                    return true;
                }
                DataAvailability hasData = mDataSource.hasDataAvailable(mSampleTable.getOffset(mCurrentSampleIndex),
                        mSampleTable.getSize(mCurrentSampleIndex));
                if (hasData == DataAvailability.NOT_AVAILABLE) {
                    mDataSource.seek(mSampleTable.getOffset(mCurrentSampleIndex));
                }
                return hasData != DataAvailability.IN_FUTURE;
            }
        }

        private boolean fillFragmentQueue() {
            long contentLength = 0;
            try {
                contentLength = mDataSource.length();
            } catch (IOException e) {
                if (LOGS_ENABLED)
                    Log.e(TAG, "IOException when retrieving content length", e);
            }

            while (mCurrentFragmentSampleQueue == null || (mCurrentFragmentSampleQueue.isEmpty()
                    && mNextMoofOffset > 0 && (mNextMoofOffset < contentLength || contentLength == -1))) {
                mCurrentTrackId = mTrackId;

                if (mNextMoofOffset == 0) {
                    // Find initial moof for this track
                    mCurrentOffset = mFirstMoofOffset;
                    mNextMoofOffset = findNextMoofForTrack(mTrackId);
                }

                if (mNextMoofOffset < 0) {
                    if (LOGS_ENABLED)
                        Log.i(TAG, "Could not find any more moof boxes for this track");
                    break;
                }

                mCurrentOffset = mNextMoofOffset;
                BoxHeader header = getNextBoxHeader();
                if (header == null || header.boxType != BOX_ID_MOOF) {
                    // We have read all moof boxes, no more data
                    if (LOGS_ENABLED)
                        Log.i(TAG, "no more moof boxes, all available data queued");
                    break;
                }
                boolean parseOk = parseBox(header);
                if (!parseOk) {
                    if (LOGS_ENABLED)
                        Log.e(TAG, "error parsing next 'moof' box");
                    return false;
                }

                mCurrentOffset = header.startOffset + header.boxHeaderSize + header.boxDataSize;
                mNextMoofOffset = findNextMoofForTrack(mTrackId);
            }
            return true;
        }
    }

    static class Trex {
        public int defaultSampleDuration = 0;

        public int defaultSampleSize = 0;
    }

    public static class Traf {
        public long baseDataOffset = Integer.MIN_VALUE;

        public int defaultSampleDuration = Integer.MIN_VALUE;

        public int defaultSampleSize = Integer.MIN_VALUE;
    }

    static class FragmentSample {
        public int durationTicks = 0;

        public int size = 0;

        public int compositionTimeOffset = 0;

        public long dataOffset = 0;

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("offset = " + dataOffset);
            sb.append(", size = " + size);
            sb.append(", duration = " + durationTicks);
            return sb.toString();
        }
    }

    static class Tfra {
        long timeTicks = 0;

        long moofOffset = 0;

        long sampleNumber = 0;
    }

    @Override
    public long getDurationUs() {
        long durationUs = 0;
        for (int i = 0; i < mTracks.size(); i++) {
            long trackDurationUs = ((IsoTrack) mTracks.get(i)).getDurationUs();
            if (trackDurationUs > durationUs) {
                durationUs = trackDurationUs;
            }
            long fileDurationUs = getLong(KEY_DURATION);
            fileDurationUs *= 1000;
            if (fileDurationUs > durationUs) {
                durationUs = fileDurationUs;
            }
        }
        return durationUs;
    }

    @Override
    public MediaFormat getFormat(TrackType type) {
        if (type == TrackType.AUDIO && mCurrentAudioTrack != null) {
            return mCurrentAudioTrack.getMediaFormat();
        } else if (type == TrackType.VIDEO && mCurrentVideoTrack != null) {
            return mCurrentVideoTrack.getMediaFormat();
        } else if (type == TrackType.SUBTITLE && mCurrentSubtitleTrack != null) {
            return mCurrentSubtitleTrack.getMediaFormat();
        }
        return null;
    }

    @Override
    public TrackInfo[] getTrackInfo() {
        int numTracks = mTracks.size();
        TrackInfo[] trackInfos = new TrackInfo[numTracks];

        for (int i = 0; i < numTracks; i++) {
            IsoTrack track = (IsoTrack) mTracks.get(i);
            TrackType trackType = track.getTrackType();
            if (trackType != TrackType.UNKNOWN) {
                MediaFormat mediaFormat = track.getMediaFormat();
                String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
                long durationUs = track.getDurationUs();
                String language = track.getLanguage();

                TrackRepresentation representation = null;

                if (trackType == TrackType.AUDIO) {
                    int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                    int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                    representation = new AudioTrackRepresentation(-1, channelCount, "", sampleRate);
                } else if (trackType == TrackType.VIDEO) {
                    int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
                    int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
                    representation = new VideoTrackRepresentation(-1, width, height, -1f);
                } else { // SUBTITLE, UNKNOWN
                    representation = new TrackRepresentation(-1);
                }

                TrackInfo trackInfo = new TrackInfo(trackType, mimeType, durationUs, language);
                TrackRepresentation[] representations = new TrackRepresentation[1];
                representations[0] = representation;
                trackInfo.setRepresentations(representations);
                trackInfos[i] = trackInfo;
            } else {
                trackInfos[i] = new TrackInfo(TrackType.UNKNOWN, "", 0, "");
            }
        }

        return trackInfos;
    }

    @Override
    public synchronized void seekTo(long seekTimeUs) {
        // Seek video to nearest previous sync sample
        // Then use timestamp of that sample to seek audio

        long timeUs = seekTimeUs;
        if (mCurrentVideoTrack != null) {
            timeUs = mCurrentVideoTrack.seekTo(seekTimeUs, mIsFragmented);
        }

        if (timeUs >= 0) {
            if (mCurrentAudioTrack != null) {
                mCurrentAudioTrack.seekTo(timeUs, mIsFragmented);
            }
            if (mCurrentSubtitleTrack != null) {
                mCurrentSubtitleTrack.seekTo(timeUs, mIsFragmented);
            }
        }
    }

    @Override
    public TrackType selectTrack(boolean select, int index) {
        if (index < 0 || index > mTracks.size()) {
            if (LOGS_ENABLED)
                Log.e(TAG, "Invalid track: " + index);
            return TrackType.UNKNOWN;
        }

        IsoTrack track = (IsoTrack) mTracks.get(index);
        TrackType trackType = track.getTrackType();

        if (trackType == TrackType.AUDIO) {
            if (select) {
                if (mCurrentAudioTrack == track) {
                    if (LOGS_ENABLED)
                        Log.w(TAG, "track " + index + " is already selected");
                    return TrackType.UNKNOWN;
                }

                long timeUs = 0;

                if (mCurrentAudioTrack != null) {
                    mCurrentAudioTrack.releaseSampleTable();
                    timeUs = mCurrentAudioTrack.getLastTimestampUs();
                } else if (mCurrentVideoTrack != null) {
                    timeUs = mCurrentVideoTrack.getLastTimestampUs();
                } else if (mCurrentSubtitleTrack != null) {
                    timeUs = mCurrentSubtitleTrack.getLastTimestampUs();
                }

                mCurrentAudioTrack = track;
                mCurrentAudioTrack.buildSampleTable();

                mCurrentAudioTrack.seekTo(timeUs, mIsFragmented);

                return TrackType.AUDIO;
            } else {
                if (LOGS_ENABLED)
                    Log.w(TAG, "Audio tracks can't be deselected");
                return TrackType.UNKNOWN;
            }
        } else if (trackType == TrackType.SUBTITLE) {
            if (select) {
                if (mCurrentSubtitleTrack == track) {
                    if (LOGS_ENABLED)
                        Log.w(TAG, "track " + index + " is already selected");
                    return TrackType.UNKNOWN;
                }

                long timeUs = 0;

                if (mCurrentSubtitleTrack != null) {
                    mCurrentSubtitleTrack.releaseSampleTable();
                    timeUs = mCurrentSubtitleTrack.getLastTimestampUs();
                } else if (mCurrentAudioTrack != null) {
                    timeUs = mCurrentAudioTrack.getLastTimestampUs();
                } else if (mCurrentVideoTrack != null) {
                    timeUs = mCurrentVideoTrack.getLastTimestampUs();
                }

                mCurrentSubtitleTrack = track;
                mCurrentSubtitleTrack.buildSampleTable();

                mCurrentSubtitleTrack.seekTo(timeUs, mIsFragmented);

                return TrackType.SUBTITLE;
            } else {
                if (mCurrentSubtitleTrack != track) {
                    if (LOGS_ENABLED)
                        Log.w(TAG, "track " + index + " is not selected");
                    return TrackType.UNKNOWN;
                }

                mCurrentSubtitleTrack.releaseSampleTable();
                mCurrentSubtitleTrack = null;

                return TrackType.SUBTITLE;
            }
        } else if (trackType == TrackType.VIDEO) {
            if (LOGS_ENABLED)
                Log.w(TAG, "Video tracks can't be changed");
            return TrackType.UNKNOWN;
        }

        return TrackType.UNKNOWN;
    }

    @Override
    public int getSelectedTrackIndex(TrackType type) {
        if (type == TrackType.AUDIO && mCurrentAudioTrack != null) {
            return mCurrentAudioTrack.getTrackIndex();
        } else if (type == TrackType.VIDEO && mCurrentVideoTrack != null) {
            return mCurrentVideoTrack.getTrackIndex();
        } else if (type == TrackType.SUBTITLE && mCurrentSubtitleTrack != null) {
            return mCurrentSubtitleTrack.getTrackIndex();
        }
        return -1;
    }

    @Override
    public boolean canParse() {
        BoxHeader header = getNextBoxHeader();
        if (header == null || header.boxType != BOX_ID_FTYP) {
            if (LOGS_ENABLED)
                Log.w(TAG, "ftyp box not found at start of file, assume not ISO compatible for now");

            return false;
        }
        try {
            int majorBrand = mDataSource.readInt();
            mDataSource.skipBytes(4); // minor_version
            int numCompatibilityBrands = (int) ((header.boxDataSize - 8) / 4);
            if (numCompatibilityBrands == 0) {
                for (int j = 0; j < ISOBMFF_COMPATIBLE_BRANDS.length; j++) {
                    if (ISOBMFF_COMPATIBLE_BRANDS[j] == majorBrand) {
                        return true;
                    }
                }
            }
            for (int i = 0; i < numCompatibilityBrands; i++) {
                int brand = mDataSource.readInt();
                for (int j = 0; j < ISOBMFF_COMPATIBLE_BRANDS.length; j++) {
                    if (ISOBMFF_COMPATIBLE_BRANDS[j] == brand) {
                        return true;
                    }
                }
            }
        } catch (EOFException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "EOFException in canParse()", e);
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "IOException in canParse()", e);
        }
        return false;
    }

    /* MetaData interface functions */

    @Override
    public int getInteger(String key) {
        if (mMetaDataValues.containsKey(key)) {
            Integer val = (Integer) mMetaDataValues.get(key);
            return val.intValue();
        }
        return Integer.MIN_VALUE;
    }

    @Override
    public long getLong(String key) {
        if (mMetaDataValues.containsKey(key)) {
            Long val = (Long) (mMetaDataValues.get(key));
            return val.longValue();
        }
        return Long.MIN_VALUE;
    }

    @Override
    public float getFloat(String key) {
        if (mMetaDataValues.containsKey(key)) {
            Float val = (Float) (mMetaDataValues.get(key));
            return val.floatValue();
        }
        return Float.MIN_VALUE;
    }

    @Override
    public String getString(String key) {
        Object value = mMetaDataValues.get(key);
        if (value == null) {
            return null;
        }
        return value.toString();
    }

    @Override
    public String getString(String key1, String key2) {
        // not applicable for this class
        return null;
    }

    @Override
    public byte[] getByteBuffer(String key) {
        return (byte[]) (mMetaDataValues.get(key));
    }

    @Override
    public byte[] getByteBuffer(String key1, String key2) {
        // not applicable for this class
        return null;
    }

    @Override
    public String[] getStringArray(String key) {
        if (mMetaDataValues.containsKey(key)) {
            Object[] values = (Object[]) mMetaDataValues.get(key);
            String[] strings = new String[values.length];
            System.arraycopy(values, 0, strings, 0, values.length);
            return strings;
        }
        return null;
    }

    @Override
    public boolean containsKey(String key) {
        return mMetaDataValues.containsKey(key);
    }

    @Override
    protected Track createTrack() {
        return new IsoTrack();
    }

    public long getMoofDataSize() {
        return mMoofDataSize;
    }

    private long findNextMoofForTrack(int trackId) {
        BoxHeader header;
        long moofOffset = Integer.MIN_VALUE;
        boolean parseOk = true;
        long sourceLength = -1;
        try {
            sourceLength = mDataSource.length();
        } catch (IOException e) {
            if (LOGS_ENABLED)
                Log.e(TAG, "could not read length of data source", e);
            return Integer.MIN_VALUE;
        }
        mDesiredTrackIdFound = false;
        mSkipInsertSamples = true;
        do {
            mCurrentMoofTrackId = -1;
            header = getNextBoxHeader();
            if (header == null) {
                return -1;
            }
            parseOk = parseBox(header);
            if (header.boxType == BOX_ID_MOOF) {
                moofOffset = header.startOffset;
            }
        } while (!mDesiredTrackIdFound && parseOk && (mCurrentOffset < sourceLength || sourceLength == -1));
        if (!parseOk || (mCurrentOffset >= sourceLength && sourceLength != -1)) {
            if (LOGS_ENABLED)
                Log.i(TAG, "Did not find any later moof for track " + trackId);
            moofOffset = Integer.MIN_VALUE;
        }
        mSkipInsertSamples = false;
        return moofOffset;
    }

    private int parseUE(BitReader br) {
        int leadingZeroBits = 0;
        while (br.getBits(1) == 0) {
            leadingZeroBits++;
        }

        int codeNum = (1 << leadingZeroBits) - 1 + br.getBits(leadingZeroBits);

        return codeNum;
    }

    protected void parseSPS(byte[] spsData) {
        BitReader br = new BitReader(spsData);
        br.skipBits(4 * 8); // NAL marker 00 00 00 01
        br.skipBits(3); // NAL Header - forbidden_zero_bit(1) + nal_ref_idc(2)

        int nal_unit_type = br.getBits(5);

        if (nal_unit_type != AVC_NAL_UNIT_TYPE_SPS) {
            return;
        }

        int profile_idc = br.getBits(8);
        br.skipBits(8); // constraint_set0_flag ... constraint_set5 _flag +
                        // reserved_zero_2bits
        br.getBits(8); // level_idc
        parseUE(br); // seq_parameter_set_id

        if (profile_idc == 100 || profile_idc == 110 || profile_idc == 122 || profile_idc == 244
                || profile_idc == 44 || profile_idc == 83 || profile_idc == 86 || profile_idc == 118
                || profile_idc == 128 || profile_idc == 138) {
            int chroma_format_idc = parseUE(br);
            if (chroma_format_idc == 3) {
                br.getBits(1); // separate_colour_plane_flag
            }

            parseUE(br); // bit_depth_luma_minus8
            parseUE(br); // bit_depth_chroma_minus8
            br.getBits(1); // qpprime_y_zero_transform_bypass_flag

            int seq_scaling_matrix_present_flag = br.getBits(1);
            if (seq_scaling_matrix_present_flag == 1) {
                int nbrScaling = 8;
                if (chroma_format_idc == 3) {
                    nbrScaling = 12;
                }

                for (int i = 0; i < nbrScaling; ++i) {
                    br.getBits(1); // seq_scaling_list_present_flag[ i ]
                }
            }
        }

        parseUE(br); // log2_max_frame_num_minus4
        int pic_order_cnt_type = parseUE(br);
        if (pic_order_cnt_type == 0) {
            parseUE(br); // log2_max_pic_order_cnt_lsb_minus4
        } else if (pic_order_cnt_type == 1) {
            br.getBits(1); // delta_pic_order_always_zero_flag
            parseUE(br); // offset_for_non_ref_pic TODO: parseSE
            parseUE(br); // offset_for_top_to_bottom_field TODO: parseSE

            int num_ref_frames_in_pic_order_cnt_cycle = parseUE(br);
            for (int i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; ++i) {
                parseUE(br); // offset_for_ref_frame[ i ] TODO: parseSE
            }
        }

        parseUE(br); // max_num_ref_frames
        br.getBits(1); // gaps_in_frame_num_value_allowed_flag

        parseUE(br); // pic_width_in_mbs_minus1
        parseUE(br); // pic_height_in_map_units_minus1
        int frame_mbs_only_flag = br.getBits(1);
        if (frame_mbs_only_flag == 0) {
            br.getBits(1); // mb_adaptive_frame_field_flag
        }

        br.getBits(1); // direct_8x8_inference_flag
        int frame_cropping_flag = br.getBits(1);
        if (frame_cropping_flag == 1) {
            parseUE(br); // frame_crop_left_offset
            parseUE(br); // frame_crop_right_offset
            parseUE(br); // frame_crop_top_offset
            parseUE(br); // frame_crop_bottom_offset
        }

        int vui_parameters_present_flag = br.getBits(1);
        if (vui_parameters_present_flag == 1) {
            int aspect_ratio_info_present_flag = br.getBits(1);
            if (aspect_ratio_info_present_flag == 1) {
                int aspect_ratio_idc = br.getBits(8);

                int sar_width = 0;
                int sar_height = 0;
                if (aspect_ratio_idc > 0 && aspect_ratio_idc < 17) {
                    int[] width = { 1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2 };
                    int[] height = { 1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1 };

                    sar_width = width[aspect_ratio_idc - 1];
                    sar_height = height[aspect_ratio_idc - 1];
                } else if (aspect_ratio_idc == 255) {
                    sar_width = br.getBits(16);
                    sar_height = br.getBits(16);
                }

                if (mCurrentMediaFormat != null && sar_width > 0 && sar_height > 0) {
                    mCurrentMediaFormat.setInteger(KEY_SAR_WIDTH, sar_width);
                    mCurrentMediaFormat.setInteger(KEY_SAR_HEIGHT, sar_height);
                }
            }
        }
        // TODO: parse the rest of vui_parameters
    }

    @Override
    public synchronized boolean hasDataAvailable(TrackType type) throws IOException {
        boolean hasData = true;

        if (type == TrackType.AUDIO && mCurrentAudioTrack != null) {
            hasData = mCurrentAudioTrack.hasDataAvailable(mIsFragmented);
        } else if (type == TrackType.VIDEO && mCurrentVideoTrack != null) {
            hasData = mCurrentVideoTrack.hasDataAvailable(mIsFragmented);
        } else if (type == TrackType.SUBTITLE && mCurrentSubtitleTrack != null) {
            hasData = mCurrentSubtitleTrack.hasDataAvailable(mIsFragmented);
        }

        return hasData;
    }
}