text: // Generated by CoffeeScript 2.4.1
  (function() {
    var AVCConfigurationBox, AVCSampleEntry, AudioSampleEntry, Bits, Box, CTOOBox, ChunkOffsetBox, CompositionOffsetBox, Container, DEBUG, DEBUG_OUTGOING_MP4_DATA, DataEntryUrlBox, DataEntryUrnBox, DataInformationBox, DataReferenceBox, ESDBox, EditBox, EditListBox, EventEmitterModule, FileTypeBox, FreeSpaceBox, GenericDataBox, GoogleGSHHBox, GoogleGSPMBox, GoogleGSPUBox, GoogleGSSDBox, GoogleGSSTBox, GoogleGSTDBox, HandlerBox, HintSampleEntry, ItemInfoBox, ItemInfoEntry, ItemLocationBox, ItemProtectionBox, MIN_TIME_DIFF, MP4AudioSampleEntry, MP4File, MPEG4BitRateBox, MPEG4ExtensionDescriptorsBox, MediaBox, MediaDataBox, MediaHeaderBox, MediaInformationBox, MetaBox, MetadataItemListBox, MovieBox, MovieHeaderBox, ObjectDescriptorBox, PrimaryItemBox, QUEUE_BUFFER_TIME, READ_BUFFER_TIME, SampleDescriptionBox, SampleSizeBox, SampleTableBox, SampleToChunkBox, Sequent, SoundMediaHeaderBox, SyncSampleBox, TAG_CTOO, TimeToSampleBox, TrackBox, TrackHeaderBox, UserDataBox, VideoMediaHeaderBox, VisualSampleEntry, api, formatDate, fs, getCurrentTime, h264, logger;
  
    Bits = require('./bits');
  
    EventEmitterModule = require('./event_emitter');
  
    Sequent = require('sequent');
  
    fs = require('fs');
  
    logger = require('./logger');
  
    h264 = require('./h264');
  
    formatDate = function(date) {
      return date.toISOString();
    };
  
    // copyright sign + 'too' (we should not use literal '\xa9'
    // since it expands to [0xc2, 0xa9])
    TAG_CTOO = new Buffer([0xa9, 0x74, 0x6f, 0x6f]).toString('utf8');
  
    MIN_TIME_DIFF = 0.01; // seconds
  
    READ_BUFFER_TIME = 3.0;
  
    QUEUE_BUFFER_TIME = 1.5;
  
    DEBUG = false;
  
    // If true, outgoing audio/video packets will be printed
    DEBUG_OUTGOING_MP4_DATA = false;
  
    getCurrentTime = function() {
      var time;
      time = process.hrtime();
      return time[0] + time[1] / 1e9;
    };
  
    MP4File = class MP4File extends EventEmitterModule {
      constructor(filename) {
        super();
        if (filename != null) {
          this.open(filename);
        }
        this.isStopped = false;
      }
  
      clearBuffers() {
        this.consumedAudioChunks = 0;
        this.consumedVideoChunks = 0;
        this.bufferedAudioTime = 0;
        this.bufferedVideoTime = 0;
        this.queuedAudioTime = 0;
        this.queuedVideoTime = 0;
        this.bufferedAudioSamples = [];
        this.queuedAudioSampleIndex = 0;
        this.bufferedVideoSamples = [];
        this.queuedVideoSampleIndex = 0;
        this.isAudioEOF = false;
        this.isVideoEOF = false;
        return this.sessionId++;
      }
  
      open(filename) {
        var diffTime, startTime;
        this.filename = filename;
        if (DEBUG) {
          startTime = process.hrtime();
        }
        this.fileBuf = fs.readFileSync(filename); // up to 1GB
        this.bits = new Bits(this.fileBuf);
        if (DEBUG) {
          diffTime = process.hrtime(startTime);
          logger.debug(`[mp4] took ${(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to read ${filename}`);
        }
        this.consumedAudioSamples = 0;
        this.consumedVideoSamples = 0;
        this.clearBuffers();
        this.currentPlayTime = 0;
        this.playStartTime = null;
        // sessionId will change when buffer is cleared
        return this.sessionId = 0;
      }
  
      close() {
        logger.debug(`[mp4:${this.filename}] close`);
        if (!this.isStopped) {
          this.stop();
        }
        this.bits = null;
        this.fileBuf = null;
        this.boxes = null;
        this.moovBox = null;
        this.mdatBox = null;
        this.audioTrakBox = null;
        this.videoTrakBox = null;
      }
  
      parse() {
        var box, child, diffTime, k, len1, ref, startTime, tkhdBox;
        if (DEBUG) {
          startTime = process.hrtime();
        }
        this.boxes = [];
        while (this.bits.has_more_data()) {
          box = Box.parse(this.bits, null); // null == root box
          if (box instanceof MovieBox) {
            this.moovBox = box;
          } else if (box instanceof MediaDataBox) {
            this.mdatBox = box;
          }
          this.boxes.push(box);
        }
        if (DEBUG) {
          diffTime = process.hrtime(startTime);
          logger.debug(`[mp4] took ${(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to parse ${this.filename}`);
        }
        ref = this.moovBox.children;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          child = ref[k];
          if (child instanceof TrackBox) { // trak
            tkhdBox = child.find('tkhd');
            if (tkhdBox.isAudioTrack) {
              this.audioTrakBox = child;
            } else {
              this.videoTrakBox = child;
            }
          }
        }
        this.numVideoSamples = this.getNumVideoSamples();
        this.numAudioSamples = this.getNumAudioSamples();
      }
  
      getTree() {
        var box, k, len1, ref, tree;
        if (this.boxes == null) {
          throw new Error("parse() must be called before dump");
        }
        tree = {
          root: []
        };
        ref = this.boxes;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          box = ref[k];
          tree.root.push(box.getTree());
        }
        return tree;
      }
  
      dump() {
        var box, k, len1, ref;
        if (this.boxes == null) {
          throw new Error("parse() must be called before dump");
        }
        ref = this.boxes;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          box = ref[k];
          process.stdout.write(box.dump(0, 2));
        }
      }
  
      hasVideo() {
        return this.videoTrakBox != null;
      }
  
      hasAudio() {
        return this.audioTrakBox != null;
      }
  
      getSPS() {
        var avcCBox;
        avcCBox = this.videoTrakBox.find('avcC');
        return avcCBox.sequenceParameterSets[0];
      }
  
      getPPS() {
        var avcCBox;
        avcCBox = this.videoTrakBox.find('avcC');
        return avcCBox.pictureParameterSets[0];
      }
  
      getAudioSpecificConfig() {
        var esdsBox;
        esdsBox = this.audioTrakBox.find('esds');
        return esdsBox.decoderConfigDescriptor.decoderSpecificInfo.specificInfo;
      }
  
      stop() {
        return this.isStopped = true;
      }
  
      isPaused() {
        return this.isStopped;
      }
  
      pause() {
        if (!this.isStopped) {
          this.isStopped = true;
          return logger.debug(`[mp4:${this.filename}] paused at ${this.currentPlayTime} (server mp4 head time)`);
        } else {
          return logger.debug(`[mp4:${this.filename}] already paused`);
        }
      }
  
      sendVideoPacketsSinceLastKeyFrame(endSeconds, callback) {
        var isFirstSample, isKeyFrameFound, k, l, len1, len2, nalUnit, nalUnits, rawSample, sample, samples, stblBox, sttsBox, videoSample, videoSampleNumber;
        if (this.videoTrakBox == null) { // video trak does not exist
          if (typeof callback === "function") {
            callback(null);
          }
          return;
        }
        // Get next sample number
        stblBox = this.videoTrakBox.child('mdia').child('minf').child('stbl');
        sttsBox = stblBox.child('stts'); // TimeToSampleBox
        videoSample = sttsBox.getSampleAfterSeconds(endSeconds);
        if (videoSample != null) {
          videoSampleNumber = videoSample.sampleNumber;
        } else {
          videoSampleNumber = this.numVideoSamples + 1;
        }
        samples = [];
        isFirstSample = true;
        while (true) {
          rawSample = this.getSample(videoSampleNumber, this.videoTrakBox);
          isKeyFrameFound = false;
          if (rawSample != null) {
            nalUnits = this.parseH264Sample(rawSample.data);
            for (k = 0, len1 = nalUnits.length; k < len1; k++) {
              nalUnit = nalUnits[k];
              if ((nalUnit[0] & 0x1f) === h264.NAL_UNIT_TYPE_IDR_PICTURE) {
                isKeyFrameFound = true;
                break;
              }
            }
            if (!isFirstSample) {
              samples.unshift({
                pts: rawSample.pts,
                dts: rawSample.dts,
                time: rawSample.time,
                data: nalUnits
              });
            }
          }
          if (isFirstSample) {
            isFirstSample = false;
          }
          if (isKeyFrameFound) {
            break;
          }
          videoSampleNumber--;
          if (videoSampleNumber <= 0) {
            break;
          }
        }
        for (l = 0, len2 = samples.length; l < len2; l++) {
          sample = samples[l];
          this.emit('video_data', sample.data, sample.pts, sample.dts);
        }
        return typeof callback === "function" ? callback(null) : void 0;
      }
  
      resume() {
        return this.play();
      }
  
      isAudioEOFReached() {
        return (this.bufferedAudioSamples.length === 0) && (this.consumedAudioSamples === this.numAudioSamples);
      }
  
      isVideoEOFReached() {
        return (this.bufferedVideoSamples.length === 0) && (this.consumedVideoSamples === this.numVideoSamples);
      }
  
      fillBuffer(callback) {
        var seq;
        seq = new Sequent;
        this.bufferAudio(() => {
          // audio samples has been buffered
          return seq.done();
        });
        this.bufferVideo(() => {
          // video samples has been buffered
          return seq.done();
        });
        return seq.wait(2, callback);
      }
  
      seek(seekSeconds = 0) {
        var audioSample, audioSampleNumber, minTime, stblBox, sttsBox, videoSample, videoSampleNumber, videoSampleSeconds;
        logger.debug(`[mp4:${this.filename}] seek: seconds=${seekSeconds}`);
        this.clearBuffers();
        if (this.videoTrakBox != null) {
          // Seek video sample
          stblBox = this.videoTrakBox.child('mdia').child('minf').child('stbl');
          sttsBox = stblBox.child('stts'); // TimeToSampleBox
          videoSample = sttsBox.getSampleAfterSeconds(seekSeconds);
          if (videoSample != null) {
            logger.debug(`video sample >= ${seekSeconds} is ${JSON.stringify(videoSample)}`);
            videoSampleSeconds = videoSample.seconds;
            this.currentPlayTime = videoSampleSeconds;
            videoSampleNumber = videoSample.sampleNumber;
          } else {
            // No video sample left
            logger.debug(`video sample >= ${seekSeconds} does not exist`);
            this.isVideoEOF = true;
            this.currentPlayTime = this.getDurationSeconds();
            videoSampleNumber = this.numVideoSamples + 1;
            videoSampleSeconds = this.currentPlayTime;
          }
        } else {
          videoSampleNumber = null;
          videoSampleSeconds = null;
        }
        if (this.audioTrakBox != null) {
          // Seek audio sample
          stblBox = this.audioTrakBox.child('mdia').child('minf').child('stbl');
          sttsBox = stblBox.child('stts'); // TimeToSampleBox
          audioSample = sttsBox.getSampleAfterSeconds(seekSeconds);
          if (audioSample != null) {
            logger.debug(`audio sample >= ${seekSeconds} is ${JSON.stringify(audioSample)}`);
            audioSampleNumber = audioSample.sampleNumber;
            if ((videoSampleSeconds != null) && (videoSampleSeconds <= audioSample.seconds)) {
              minTime = videoSampleSeconds;
            } else {
              minTime = audioSample.seconds;
            }
            if (this.currentPlayTime !== minTime) {
              this.currentPlayTime = minTime;
            }
          } else {
            // No audio sample left
            logger.debug(`audio sample >= ${seekSeconds} does not exist`);
            audioSampleNumber = this.numAudioSamples + 1;
            this.isAudioEOF = true;
          }
        } else {
          audioSampleNumber = null;
        }
        if (audioSampleNumber != null) {
          this.consumedAudioSamples = audioSampleNumber - 1;
        }
        if (videoSampleNumber != null) {
          this.consumedVideoSamples = videoSampleNumber - 1;
        }
        logger.debug(`[mp4:${this.filename}] set current play time to ${this.currentPlayTime}`);
        return this.currentPlayTime;
      }
  
      play() {
        logger.debug(`[mp4:${this.filename}] start playing from ${this.currentPlayTime} (server mp4 head time)`);
        return this.fillBuffer(() => {
          this.isStopped = false;
          this.playStartTime = getCurrentTime() - this.currentPlayTime;
          if (this.isAudioEOFReached()) {
            this.isAudioEOF = true;
          }
          if (this.isVideoEOFReached()) {
            this.isVideoEOF = true;
          }
          if (this.checkEOF()) {
            // EOF reached
            return false;
          } else {
            this.queueBufferedSamples();
            return true;
          }
        });
      }
  
      checkAudioBuffer() {
        var timeDiff;
        timeDiff = this.bufferedAudioTime - this.currentPlayTime;
        if (timeDiff < READ_BUFFER_TIME) {
          // Fill audio buffer
          if (this.readNextAudioChunk()) {
            // Audio EOF not reached
            this.queueBufferedSamples();
          }
        } else {
          this.queueBufferedSamples();
        }
      }
  
      checkVideoBuffer() {
        var timeDiff;
        timeDiff = this.bufferedVideoTime - this.currentPlayTime;
        if (timeDiff < READ_BUFFER_TIME) {
          // Fill video buffer
          if (this.readNextVideoChunk()) {
            // Video EOF not reached
            this.queueBufferedSamples();
          }
        } else {
          this.queueBufferedSamples();
        }
      }
  
      startStreaming() {
        return this.queueBufferedSamples();
      }
  
      updateCurrentPlayTime() {
        return this.currentPlayTime = getCurrentTime() - this.playStartTime;
      }
  
      queueBufferedAudioSamples() {
        var audioSample, sessionId, timeDiff;
        audioSample = this.bufferedAudioSamples[this.queuedAudioSampleIndex];
        if (audioSample == null) { // @bufferedAudioSamples is empty
          return;
        }
        timeDiff = audioSample.time - this.currentPlayTime;
        if (timeDiff <= MIN_TIME_DIFF) {
          this.bufferedAudioSamples.shift();
          this.queuedAudioSampleIndex--;
          if (DEBUG_OUTGOING_MP4_DATA) {
            logger.info(`emit audio_data pts=${audioSample.pts}`);
          }
          this.emit('audio_data', audioSample.data, audioSample.pts);
          this.updateCurrentPlayTime();
          if ((this.queuedAudioSampleIndex === 0) && (this.consumedAudioSamples === this.numAudioSamples)) {
            // No audio sample left
            this.isAudioEOF = true;
            this.checkEOF();
          }
        } else {
          if (!this.isStopped) {
            sessionId = this.sessionId;
            setTimeout(() => {
              if ((!this.isStopped) && (this.sessionId === sessionId)) {
                this.bufferedAudioSamples.shift();
                this.queuedAudioSampleIndex--;
                if (DEBUG_OUTGOING_MP4_DATA) {
                  logger.info(`emit timed audio_data pts=${audioSample.pts}`);
                }
                this.emit('audio_data', audioSample.data, audioSample.pts);
                this.updateCurrentPlayTime();
                if ((this.queuedAudioSampleIndex === 0) && (this.consumedAudioSamples === this.numAudioSamples)) {
                  // No audio sample left
                  this.isAudioEOF = true;
                  return this.checkEOF();
                } else {
                  return this.checkAudioBuffer();
                }
              }
            }, timeDiff * 1000);
          }
        }
        this.queuedAudioSampleIndex++;
        this.queuedAudioTime = audioSample.time;
        if (this.queuedAudioTime - this.currentPlayTime < QUEUE_BUFFER_TIME) {
          return this.queueBufferedSamples();
        }
      }
  
      queueBufferedVideoSamples() {
        var k, len1, nalUnit, ref, sessionId, timeDiff, totalBytes, videoSample;
        if (this.isStopped) {
          return;
        }
        videoSample = this.bufferedVideoSamples[this.queuedVideoSampleIndex];
        if (videoSample == null) { // @bufferedVideoSamples is empty
          return;
        }
        timeDiff = videoSample.time - this.currentPlayTime;
        if (timeDiff <= MIN_TIME_DIFF) {
          this.bufferedVideoSamples.shift();
          this.queuedVideoSampleIndex--;
          if (DEBUG_OUTGOING_MP4_DATA) {
            totalBytes = 0;
            ref = videoSample.data;
            for (k = 0, len1 = ref.length; k < len1; k++) {
              nalUnit = ref[k];
              totalBytes += nalUnit.length;
            }
            logger.info(`emit video_data pts=${videoSample.pts} dts=${videoSample.dts} bytes=${totalBytes}`);
          }
          this.emit('video_data', videoSample.data, videoSample.pts, videoSample.dts);
          this.updateCurrentPlayTime();
          if ((this.queuedVideoSampleIndex === 0) && (this.consumedVideoSamples === this.numVideoSamples)) {
            // No video sample left
            this.isVideoEOF = true;
            this.checkEOF();
          }
        } else {
          sessionId = this.sessionId;
          setTimeout(() => {
            var l, len2, ref1;
            if ((!this.isStopped) && (this.sessionId === sessionId)) {
              this.bufferedVideoSamples.shift();
              this.queuedVideoSampleIndex--;
              if (DEBUG_OUTGOING_MP4_DATA) {
                totalBytes = 0;
                ref1 = videoSample.data;
                for (l = 0, len2 = ref1.length; l < len2; l++) {
                  nalUnit = ref1[l];
                  totalBytes += nalUnit.length;
                }
                logger.info(`emit timed video_data pts=${videoSample.pts} dts=${videoSample.dts} bytes=${totalBytes}`);
              }
              this.emit('video_data', videoSample.data, videoSample.pts, videoSample.dts);
              this.updateCurrentPlayTime();
              if ((this.queuedVideoSampleIndex === 0) && (this.consumedVideoSamples === this.numVideoSamples)) {
                // No video sample left
                this.isVideoEOF = true;
                return this.checkEOF();
              } else {
                return this.checkVideoBuffer();
              }
            }
          }, timeDiff * 1000);
        }
        this.queuedVideoSampleIndex++;
        this.queuedVideoTime = videoSample.time;
        if (this.queuedVideoTime - this.currentPlayTime < QUEUE_BUFFER_TIME) {
          return this.queueBufferedSamples();
        }
      }
  
      queueBufferedSamples() {
        var firstAudioTime, firstVideoTime, ref, ref1;
        if (this.isStopped) {
          return;
        }
        // Determine which of audio or video should be sent first
        firstAudioTime = (ref = this.bufferedAudioSamples[this.queuedAudioSampleIndex]) != null ? ref.time : void 0;
        firstVideoTime = (ref1 = this.bufferedVideoSamples[this.queuedVideoSampleIndex]) != null ? ref1.time : void 0;
        if ((firstAudioTime != null) && (firstVideoTime != null)) {
          if (firstVideoTime <= firstAudioTime) {
            this.queueBufferedVideoSamples();
            return this.queueBufferedAudioSamples();
          } else {
            this.queueBufferedAudioSamples();
            return this.queueBufferedVideoSamples();
          }
        } else {
          this.queueBufferedAudioSamples();
          return this.queueBufferedVideoSamples();
        }
      }
  
      checkEOF() {
        if (this.isAudioEOF && this.isVideoEOF) {
          this.stop();
          this.emit('eof');
          return true;
        }
        return false;
      }
  
      bufferAudio(callback) {
        // TODO: Use async
        while (this.bufferedAudioTime < this.currentPlayTime + READ_BUFFER_TIME) {
          if (!this.readNextAudioChunk()) {
            // No audio sample left
            break;
          }
        }
        return typeof callback === "function" ? callback() : void 0;
      }
  
      bufferVideo(callback) {
        // TODO: Use async
        while (this.bufferedVideoTime < this.currentPlayTime + READ_BUFFER_TIME) {
          if (!this.readNextVideoChunk()) {
            // No video sample left
            break;
          }
        }
        return typeof callback === "function" ? callback() : void 0;
      }
  
      getNumVideoSamples() {
        var sttsBox;
        if (this.videoTrakBox != null) {
          sttsBox = this.videoTrakBox.find('stts');
          return sttsBox.getTotalSamples();
        } else {
          return 0;
        }
      }
  
      getNumAudioSamples() {
        var sttsBox;
        if (this.audioTrakBox != null) {
          sttsBox = this.audioTrakBox.find('stts');
          return sttsBox.getTotalSamples();
        } else {
          return 0;
        }
      }
  
      // Returns the timestamp of the last sample in the file
      getLastTimestamp() {
        var audioLastTimestamp, numAudioSamples, numVideoSamples, sttsBox, videoLastTimestamp;
        if (this.videoTrakBox != null) {
          numVideoSamples = this.getNumVideoSamples();
          sttsBox = this.videoTrakBox.find('stts');
          videoLastTimestamp = sttsBox.getDecodingTime(numVideoSamples).seconds;
        } else {
          videoLastTimestamp = 0;
        }
        if (this.audioTrakBox != null) {
          numAudioSamples = this.getNumAudioSamples();
          sttsBox = this.audioTrakBox.find('stts');
          audioLastTimestamp = sttsBox.getDecodingTime(numAudioSamples).seconds;
        } else {
          audioLastTimestamp = 0;
        }
        if (audioLastTimestamp > videoLastTimestamp) {
          return audioLastTimestamp;
        } else {
          return videoLastTimestamp;
        }
      }
  
      getDurationSeconds() {
        var mvhdBox;
        mvhdBox = this.moovBox.child('mvhd');
        return mvhdBox.durationSeconds;
      }
  
      parseH264Sample(buf) {
        var avcCBox, bits, length, lengthSize, nalUnits;
        // The format is defined in ISO 14496-15 5.2.3
        // <length><NAL unit> <length><NAL unit> ...
        avcCBox = this.videoTrakBox.find('avcC');
        lengthSize = avcCBox.lengthSizeMinusOne + 1;
        bits = new Bits(buf);
        nalUnits = [];
        while (bits.has_more_data()) {
          length = bits.read_bits(lengthSize * 8);
          nalUnits.push(bits.read_bytes(length));
        }
        if (bits.get_remaining_bits() !== 0) {
          throw new Error(`number of remaining bits is not zero: ${bits.get_remaining_bits()}`);
        }
        return nalUnits;
      }
  
      getSample(sampleNumber, trakBox) {
        var chunkNumber, chunkOffset, compositionTime, compositionTimeOffset, cttsBox, dts, firstSampleNumberInChunk, i, k, len1, mdhdBox, pts, sampleOffset, sampleSize, sampleSizes, sampleTime, samples, stblBox, stcoBox, stscBox, stszBox, sttsBox;
        stblBox = trakBox.child('mdia').child('minf').child('stbl');
        sttsBox = stblBox.child('stts');
        stscBox = stblBox.child('stsc');
        chunkNumber = stscBox.findChunk(sampleNumber);
        // Get chunk offset in the file
        stcoBox = stblBox.child('stco');
        chunkOffset = stcoBox.getChunkOffset(chunkNumber);
        firstSampleNumberInChunk = stscBox.getFirstSampleNumberInChunk(chunkNumber);
        // Get an array of sample sizes in this chunk
        stszBox = stblBox.child('stsz');
        sampleSizes = stszBox.getSampleSizes(firstSampleNumberInChunk, sampleNumber - firstSampleNumberInChunk + 1);
        cttsBox = stblBox.child('ctts');
        samples = [];
        sampleOffset = 0;
        mdhdBox = trakBox.child('mdia').child('mdhd');
        for (i = k = 0, len1 = sampleSizes.length; k < len1; i = ++k) {
          sampleSize = sampleSizes[i];
          if (firstSampleNumberInChunk + i === sampleNumber) {
            compositionTimeOffset = 0;
            if (cttsBox != null) {
              compositionTimeOffset = cttsBox.getCompositionTimeOffset(sampleNumber);
            }
            sampleTime = sttsBox.getDecodingTime(sampleNumber);
            compositionTime = sampleTime.time + compositionTimeOffset;
            if (mdhdBox.timescale !== 90000) {
              pts = Math.floor(compositionTime * 90000 / mdhdBox.timescale);
              dts = Math.floor(sampleTime.time * 90000 / mdhdBox.timescale);
            } else {
              pts = compositionTime;
              dts = sampleTime.time;
            }
            return {
              pts: pts,
              dts: dts,
              time: sampleTime.seconds,
              data: this.fileBuf.slice(chunkOffset + sampleOffset, chunkOffset + sampleOffset + sampleSize)
            };
          }
          sampleOffset += sampleSize;
        }
        return null;
      }
  
      readChunk(chunkNumber, fromSampleNumber, trakBox) {
        var chunkOffset, compositionTime, compositionTimeOffset, cttsBox, dts, firstSampleNumberInChunk, i, k, len1, mdhdBox, numSamplesInChunk, pts, sampleOffset, sampleSize, sampleSizes, sampleTime, samples, stblBox, stcoBox, stscBox, stszBox, sttsBox;
        stblBox = trakBox.child('mdia').child('minf').child('stbl');
        sttsBox = stblBox.child('stts');
        stscBox = stblBox.child('stsc');
        numSamplesInChunk = stscBox.getNumSamplesInChunk(chunkNumber);
        // Get chunk offset in the file
        stcoBox = stblBox.child('stco');
        chunkOffset = stcoBox.getChunkOffset(chunkNumber);
        firstSampleNumberInChunk = stscBox.getFirstSampleNumberInChunk(chunkNumber);
        // Get an array of sample sizes in this chunk
        stszBox = stblBox.child('stsz');
        sampleSizes = stszBox.getSampleSizes(firstSampleNumberInChunk, numSamplesInChunk);
        cttsBox = stblBox.child('ctts');
        samples = [];
        sampleOffset = 0;
        mdhdBox = trakBox.child('mdia').child('mdhd');
        for (i = k = 0, len1 = sampleSizes.length; k < len1; i = ++k) {
          sampleSize = sampleSizes[i];
          if (firstSampleNumberInChunk + i >= fromSampleNumber) {
            compositionTimeOffset = 0;
            if (cttsBox != null) {
              compositionTimeOffset = cttsBox.getCompositionTimeOffset(firstSampleNumberInChunk + i);
            }
            sampleTime = sttsBox.getDecodingTime(firstSampleNumberInChunk + i);
            compositionTime = sampleTime.time + compositionTimeOffset;
            if (mdhdBox.timescale !== 90000) {
              pts = Math.floor(compositionTime * 90000 / mdhdBox.timescale);
              dts = Math.floor(sampleTime.time * 90000 / mdhdBox.timescale);
            } else {
              pts = compositionTime;
              dts = sampleTime.time;
            }
            samples.push({
              pts: pts,
              dts: dts,
              time: sampleTime.seconds,
              data: this.fileBuf.slice(chunkOffset + sampleOffset, chunkOffset + sampleOffset + sampleSize)
            });
          }
          sampleOffset += sampleSize;
        }
        return samples;
      }
  
      readNextVideoChunk() {
        var chunkNumber, k, len1, nalUnits, numSamples, sample, samples, stscBox;
        if (this.consumedVideoSamples >= this.numVideoSamples) {
          return false;
        }
        if (this.consumedVideoChunks === 0 && this.consumedVideoSamples !== 0) { // seeked
          stscBox = this.videoTrakBox.find('stsc');
          chunkNumber = stscBox.findChunk(this.consumedVideoSamples + 1);
          samples = this.readChunk(chunkNumber, this.consumedVideoSamples + 1, this.videoTrakBox);
          this.consumedVideoChunks = chunkNumber;
        } else {
          samples = this.readChunk(this.consumedVideoChunks + 1, this.consumedVideoSamples + 1, this.videoTrakBox);
          this.consumedVideoChunks++;
        }
        for (k = 0, len1 = samples.length; k < len1; k++) {
          sample = samples[k];
          nalUnits = this.parseH264Sample(sample.data);
          sample.data = nalUnits;
        }
        numSamples = samples.length;
        this.consumedVideoSamples += numSamples;
        this.bufferedVideoTime = samples[numSamples - 1].time;
        this.bufferedVideoSamples = this.bufferedVideoSamples.concat(samples);
        return true;
      }
  
      parseAACSample(buf) {}
  
      // nop
      readNextAudioChunk() {
        var chunkNumber, samples, stscBox;
        if (this.consumedAudioSamples >= this.numAudioSamples) {
          return false;
        }
        if (this.consumedAudioChunks === 0 && this.consumedAudioSamples !== 0) { // seeked
          stscBox = this.audioTrakBox.find('stsc');
          chunkNumber = stscBox.findChunk(this.consumedAudioSamples + 1);
          samples = this.readChunk(chunkNumber, this.consumedAudioSamples + 1, this.audioTrakBox);
          this.consumedAudioChunks = chunkNumber;
        } else {
          samples = this.readChunk(this.consumedAudioChunks + 1, this.consumedAudioSamples + 1, this.audioTrakBox);
          this.consumedAudioChunks++;
        }
        //    for sample in samples
        //      @parseAACSample sample.data
        this.consumedAudioSamples += samples.length;
        this.bufferedAudioTime = samples[samples.length - 1].time;
        this.bufferedAudioSamples = this.bufferedAudioSamples.concat(samples);
        return true;
      }
  
    };
  
    Box = class Box {
      // time: seconds since midnight, Jan. 1, 1904 UTC
      static mp4TimeToDate(time) {
        return new Date(new Date('1904-01-01 00:00:00+0000').getTime() + time * 1000);
      }
  
      getTree() {
        var child, k, len1, obj, ref;
        obj = {
          type: this.typeStr
        };
        if (this.children != null) {
          obj.children = [];
          ref = this.children;
          for (k = 0, len1 = ref.length; k < len1; k++) {
            child = ref[k];
            obj.children.push(child.getTree());
          }
        }
        return obj;
      }
  
      dump(depth = 0, detailLevel = 0) {
        var child, detailString, i, k, l, len1, ref, ref1, str;
        str = '';
        for (i = k = 0, ref = depth; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          str += '  ';
        }
        str += `${this.typeStr}`;
        if (detailLevel > 0) {
          detailString = this.getDetails(detailLevel);
          if (detailString != null) {
            str += ` (${detailString})`;
          }
        }
        str += "\n";
        if (this.children != null) {
          ref1 = this.children;
          for (l = 0, len1 = ref1.length; l < len1; l++) {
            child = ref1[l];
            str += child.dump(depth + 1, detailLevel);
          }
        }
        return str;
      }
  
      getDetails(detailLevel) {
        return null;
      }
  
      constructor(info) {
        var name, value;
        for (name in info) {
          value = info[name];
          this[name] = value;
        }
        if (this.data != null) {
          this.read(this.data);
        }
      }
  
      readFullBoxHeader(bits) {
        this.version = bits.read_byte();
        this.flags = bits.read_bits(24);
      }
  
      findParent(typeStr) {
        if (this.parent != null) {
          if (this.parent.typeStr === typeStr) {
            return this.parent;
          } else {
            return this.parent.findParent(typeStr);
          }
        } else {
          return null;
        }
      }
  
      child(typeStr) {
        var child, k, len1, ref;
        if (this.typeStr === typeStr) {
          return this;
        } else {
          if (this.children != null) {
            ref = this.children;
            for (k = 0, len1 = ref.length; k < len1; k++) {
              child = ref[k];
              if (child.typeStr === typeStr) {
                return child;
              }
            }
          }
          return null;
        }
      }
  
      find(typeStr) {
        var box, child, k, len1, ref;
        if (this.typeStr === typeStr) {
          return this;
        } else {
          if (this.children != null) {
            ref = this.children;
            for (k = 0, len1 = ref.length; k < len1; k++) {
              child = ref[k];
              box = child.find(typeStr);
              if (box != null) {
                return box;
              }
            }
          }
          return null;
        }
      }
  
      read(buf) {}
  
      static readHeader(bits, destObj) {
        var headerLen;
        destObj.size = bits.read_uint32();
        destObj.type = bits.read_bytes(4);
        destObj.typeStr = destObj.type.toString('utf8');
        headerLen = 8;
        if (destObj.size === 1) {
          destObj.size = bits.read_bits(64); // TODO: might lose some precision
          headerLen += 8;
        }
        if (destObj.typeStr === 'uuid') {
          destObj.usertype = bits.read_bytes(16);
          headerLen += 16;
        }
        if (destObj.size > 0) {
          destObj.data = bits.read_bytes(destObj.size - headerLen);
        } else {
          destObj.data = bits.remaining_buffer();
          destObj.size = headerLen + destObj.data.length;
        }
      }
  
      static readLanguageCode(bits) {
        return Box.readASCII(bits) + Box.readASCII(bits) + Box.readASCII(bits);
      }
  
      static readASCII(bits) {
        var diff;
        diff = bits.read_bits(5);
        return String.fromCharCode(0x60 + diff);
      }
  
      static parse(bits, parent = null, cls) {
        var info;
        info = {};
        info.parent = parent;
        this.readHeader(bits, info);
        switch (info.typeStr) {
          case 'ftyp':
            return new FileTypeBox(info);
          case 'moov':
            return new MovieBox(info);
          case 'mvhd':
            return new MovieHeaderBox(info);
          case 'mdat':
            return new MediaDataBox(info);
          case 'trak':
            return new TrackBox(info);
          case 'tkhd':
            return new TrackHeaderBox(info);
          case 'edts':
            return new EditBox(info);
          case 'elst':
            return new EditListBox(info);
          case 'mdia':
            return new MediaBox(info);
          case 'iods':
            return new ObjectDescriptorBox(info);
          case 'mdhd':
            return new MediaHeaderBox(info);
          case 'hdlr':
            return new HandlerBox(info);
          case 'minf':
            return new MediaInformationBox(info);
          case 'vmhd':
            return new VideoMediaHeaderBox(info);
          case 'dinf':
            return new DataInformationBox(info);
          case 'dref':
            return new DataReferenceBox(info);
          case 'url ':
            return new DataEntryUrlBox(info);
          case 'urn ':
            return new DataEntryUrnBox(info);
          case 'stbl':
            return new SampleTableBox(info);
          case 'stsd':
            return new SampleDescriptionBox(info);
          case 'stts':
            return new TimeToSampleBox(info);
          case 'stss':
            return new SyncSampleBox(info);
          case 'stsc':
            return new SampleToChunkBox(info);
          case 'stsz':
            return new SampleSizeBox(info);
          case 'stco':
            return new ChunkOffsetBox(info);
          case 'smhd':
            return new SoundMediaHeaderBox(info);
          case 'meta':
            return new MetaBox(info);
          case 'pitm':
            return new PrimaryItemBox(info);
          case 'iloc':
            return new ItemLocationBox(info);
          case 'ipro':
            return new ItemProtectionBox(info);
          case 'infe':
            return new ItemInfoEntry(info);
          case 'iinf':
            return new ItemInfoBox(info);
          case 'ilst':
            return new MetadataItemListBox(info);
          case 'gsst':
            return new GoogleGSSTBox(info);
          case 'gstd':
            return new GoogleGSTDBox(info);
          case 'gssd':
            return new GoogleGSSDBox(info);
          case 'gspu':
            return new GoogleGSPUBox(info);
          case 'gspm':
            return new GoogleGSPMBox(info);
          case 'gshh':
            return new GoogleGSHHBox(info);
          case 'udta':
            return new UserDataBox(info);
          case 'avc1':
            return new AVCSampleEntry(info);
          case 'avcC':
            return new AVCConfigurationBox(info);
          case 'btrt':
            return new MPEG4BitRateBox(info);
          case 'm4ds':
            return new MPEG4ExtensionDescriptorsBox(info);
          case 'mp4a':
            return new MP4AudioSampleEntry(info);
          case 'esds':
            return new ESDBox(info);
          case 'free':
            return new FreeSpaceBox(info);
          case 'ctts':
            return new CompositionOffsetBox(info);
          case TAG_CTOO:
            return new CTOOBox(info);
          default:
            if (cls != null) {
              return new cls(info);
            } else {
              logger.warn(`[mp4] warning: skipping unknown (not implemented) box type: ${info.typeStr} (0x${info.type.toString('hex')})`);
              return new Box(info);
            }
        }
      }
  
    };
  
    Container = class Container extends Box {
      read(buf) {
        var bits, box;
        bits = new Bits(buf);
        this.children = [];
        while (bits.has_more_data()) {
          box = Box.parse(bits, this);
          this.children.push(box);
        }
      }
  
    };
  
    //  getDetails: (detailLevel) ->
    //    "Container"
  
    // moov
    MovieBox = class MovieBox extends Container {};
  
    // stbl
    SampleTableBox = class SampleTableBox extends Container {};
  
    // dinf
    DataInformationBox = class DataInformationBox extends Container {};
  
    // udta
    UserDataBox = class UserDataBox extends Container {};
  
    // minf
    MediaInformationBox = class MediaInformationBox extends Container {};
  
    // mdia
    MediaBox = class MediaBox extends Container {};
  
    // edts
    EditBox = class EditBox extends Container {};
  
    // trak
    TrackBox = class TrackBox extends Container {};
  
    // ftyp
    FileTypeBox = class FileTypeBox extends Box {
      read(buf) {
        var bits, brand, brandStr;
        bits = new Bits(buf);
        this.majorBrand = bits.read_uint32();
        this.majorBrandStr = Bits.uintToString(this.majorBrand, 4);
        this.minorVersion = bits.read_uint32();
        this.compatibleBrands = [];
        while (bits.has_more_data()) {
          brand = bits.read_bytes(4);
          brandStr = brand.toString('utf8');
          this.compatibleBrands.push({
            brand: brand,
            brandStr: brandStr
          });
        }
      }
  
      getDetails(detailLevel) {
        return `brand=${this.majorBrandStr} version=${this.minorVersion}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.brand = this.majorBrandStr;
        obj.version = this.minorVersion;
        return obj;
      }
  
    };
  
    // mvhd
    MovieHeaderBox = class MovieHeaderBox extends Box {
      read(buf) {
        var bits, reserved, reservedInt1, reservedInt2;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        if (this.version === 1) {
          this.creationTime = bits.read_bits(64); // TODO: loses precision
          this.creationDate = Box.mp4TimeToDate(this.creationTime);
          this.modificationTime = bits.read_bits(64); // TODO: loses precision
          this.modificationDate = Box.mp4TimeToDate(this.modificationTime);
          this.timescale = bits.read_uint32();
          this.duration = bits.read_bits(64); // TODO: loses precision
          this.durationSeconds = this.duration / this.timescale; // @version is 0
        } else {
          this.creationTime = bits.read_bits(32); // TODO: loses precision
          this.creationDate = Box.mp4TimeToDate(this.creationTime);
          this.modificationTime = bits.read_bits(32); // TODO: loses precision
          this.modificationDate = Box.mp4TimeToDate(this.modificationTime);
          this.timescale = bits.read_uint32();
          this.duration = bits.read_bits(32); // TODO: loses precision
          this.durationSeconds = this.duration / this.timescale;
        }
        this.rate = bits.read_int(32);
        if (this.rate !== 0x00010000) { // 1.0
          logger.warn(`[mp4] warning: Irregular rate found in mvhd box: ${this.rate}`);
        }
        this.volume = bits.read_int(16);
        if (this.volume !== 0x0100) { // full volume
          logger.warn(`[mp4] warning: Irregular volume found in mvhd box: ${this.volume}`);
        }
        reserved = bits.read_bits(16);
        if (reserved !== 0) {
          throw new Error(`reserved bits are not all zero: ${reserved}`);
        }
        reservedInt1 = bits.read_int(32);
        if (reservedInt1 !== 0) {
          throw new Error(`reserved int(32) (1) is not zero: ${reservedInt1}`);
        }
        reservedInt2 = bits.read_int(32);
        if (reservedInt2 !== 0) {
          throw new Error(`reserved int(32) (2) is not zero: ${reservedInt2}`);
        }
        bits.skip_bytes(4 * 9); // Unity matrix
        bits.skip_bytes(4 * 6); // pre_defined
        this.nextTrackID = bits.read_uint32();
        if (bits.has_more_data()) {
          throw new Error("mvhd box has more data");
        }
      }
  
      getDetails(detailLevel) {
        return `created=${formatDate(this.creationDate)} modified=${formatDate(this.modificationDate)} timescale=${this.timescale} durationSeconds=${this.durationSeconds}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.creationDate = this.creationDate;
        obj.modificationDate = this.modificationDate;
        obj.timescale = this.timescale;
        obj.duration = this.duration;
        obj.durationSeconds = this.durationSeconds;
        return obj;
      }
  
    };
  
    // Object Descriptor Box: contains an Object Descriptor or an Initial Object Descriptor
    // (iods)
    // Defined in ISO 14496-14
    ObjectDescriptorBox = class ObjectDescriptorBox extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
      }
  
    };
  
    // Track header box: specifies the characteristics of a single track (tkhd)
    TrackHeaderBox = class TrackHeaderBox extends Box {
      read(buf) {
        var bits, reserved;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        if (this.version === 1) {
          this.creationTime = bits.read_bits(64); // TODO: loses precision
          this.creationDate = Box.mp4TimeToDate(this.creationTime);
          this.modificationTime = bits.read_bits(64); // TODO: loses precision
          this.modificationDate = Box.mp4TimeToDate(this.modificationTime);
          this.trackID = bits.read_uint32();
          reserved = bits.read_uint32();
          if (reserved !== 0) {
            throw new Error(`tkhd: reserved bits are not zero: ${reserved}`);
          }
          this.duration = bits.read_bits(64); // TODO: loses precision
  // @version is 0
        } else {
          this.creationTime = bits.read_bits(32); // TODO: loses precision
          this.creationDate = Box.mp4TimeToDate(this.creationTime);
          this.modificationTime = bits.read_bits(32); // TODO: loses precision
          this.modificationDate = Box.mp4TimeToDate(this.modificationTime);
          this.trackID = bits.read_uint32();
          reserved = bits.read_uint32();
          if (reserved !== 0) {
            throw new Error(`tkhd: reserved bits are not zero: ${reserved}`);
          }
          this.duration = bits.read_bits(32); // TODO: loses precision
        }
        reserved = bits.read_bits(64);
        if (reserved !== 0) {
          throw new Error(`tkhd: reserved bits are not zero: ${reserved}`);
        }
        this.layer = bits.read_int(16);
        if (this.layer !== 0) {
          logger.warn(`[mp4] warning: layer is not 0 in tkhd box: ${this.layer}`);
        }
        this.alternateGroup = bits.read_int(16);
        //    if @alternateGroup isnt 0
        //      logger.warn "[mp4] warning: alternate_group is not 0 in tkhd box: #{@alternateGroup}"
        this.volume = bits.read_int(16);
        if (this.volume === 0x0100) {
          this.isAudioTrack = true;
        } else {
          this.isAudioTrack = false;
        }
        reserved = bits.read_bits(16);
        if (reserved !== 0) {
          throw new Error(`tkhd: reserved bits are not zero: ${reserved}`);
        }
        bits.skip_bytes(4 * 9);
        this.width = bits.read_uint32() / 65536; // fixed-point 16.16 value
        this.height = bits.read_uint32() / 65536; // fixed-point 16.16 value
        if (bits.has_more_data()) {
          throw new Error("tkhd box has more data");
        }
      }
  
      getDetails(detailLevel) {
        var str;
        str = `created=${formatDate(this.creationDate)} modified=${formatDate(this.modificationDate)}`;
        if (this.isAudioTrack) {
          str += " audio";
        } else {
          str += ` video; width=${this.width} height=${this.height}`;
        }
        return str;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.creationDate = this.creationDate;
        obj.modificationDate = this.modificationDate;
        obj.isAudioTrack = this.isAudioTrack;
        obj.width = this.width;
        obj.height = this.height;
        return obj;
      }
  
    };
  
    // elst
    // Edit list box: explicit timeline map
    EditListBox = class EditListBox extends Box {
      read(buf) {
        var bits, entryCount, i, k, mediaRateFraction, mediaRateInteger, mediaTime, mvhdBox, ref, segmentDuration;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        // moov
        //   mvhd <- find target
        //   iods
        //   trak
        //     tkhd
        //     edts
        //       elst <- self
        mvhdBox = this.findParent('moov').find('mvhd');
        // We cannot get mdhd box at this time, since it is not parsed yet
        entryCount = bits.read_uint32();
        this.entries = [];
        for (i = k = 1, ref = entryCount; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
          if (this.version === 1) {
            segmentDuration = bits.read_bits(64); // TODO: loses precision
            mediaTime = bits.read_int(64); // @version is 0
          } else {
            segmentDuration = bits.read_bits(32);
            mediaTime = bits.read_int(32);
          }
          mediaRateInteger = bits.read_int(16);
          mediaRateFraction = bits.read_int(16);
          if (mediaRateFraction !== 0) {
            logger.warn(`[mp4] warning: media_rate_fraction is not 0 in elst box: ${mediaRateFraction}`);
          }
          this.entries.push({
            segmentDuration: segmentDuration, // in Movie Header Box (mvhd) timescale
            segmentDurationSeconds: segmentDuration / mvhdBox.timescale,
            mediaTime: mediaTime, // in media (mdhd) timescale
            mediaRate: mediaRateInteger + mediaRateFraction / 65536 // TODO: Is this correct?
          });
        }
        if (bits.has_more_data()) {
          throw new Error("elst box has more data");
        }
      }
  
      // Returns the starting offset for this track in mdhd timescale units
      getEmptyDuration() {
        var entry, k, len1, mdhdBox, mvhdBox, ref, time;
        time = 0;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          if (entry.mediaTime === -1) { // empty edit
            // moov
            //   mvhd <- find target
            //   iods
            //   trak
            //     tkhd
            //     edts
            //       elst <- self
            mvhdBox = this.findParent('moov').child('mvhd');
            //   trak
            //     tkhd
            //     edts
            //       elst <- self
            //     mdia
            //       mdhd <- find target
            mdhdBox = this.findParent('trak').child('mdia').child('mdhd');
            if (mdhdBox == null) {
              throw new Error("cannot access mdhd box (not parsed yet?)");
            }
            // Convert segmentDuration from mvhd timescale to mdhd timescale
            time += entry.segmentDuration * mdhdBox.timescale / mvhdBox.timescale;
          } else {
            // mediaTime is already in mdhd timescale, so no conversion needed
            return time + entry.mediaTime;
          }
        }
      }
  
      getDetails(detailLevel) {
        return this.entries.map(function(entry, index) {
          return `[${index}]:segmentDuration=${entry.segmentDuration},segmentDurationSeconds=${entry.segmentDurationSeconds},mediaTime=${entry.mediaTime},mediaRate=${entry.mediaRate}`;
        }).join(',');
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.entries = this.entries;
        return obj;
      }
  
    };
  
    // Media Header Box (mdhd): declares overall information
    // Container: Media Box ('mdia')
    // Defined in ISO 14496-12
    MediaHeaderBox = class MediaHeaderBox extends Box {
      read(buf) {
        var bits, pad, pre_defined;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        if (this.version === 1) {
          this.creationTime = bits.read_bits(64); // TODO: loses precision
          this.creationDate = Box.mp4TimeToDate(this.creationTime);
          this.modificationTime = bits.read_bits(64); // TODO: loses precision
          this.modificationDate = Box.mp4TimeToDate(this.modificationTime);
          this.timescale = bits.read_uint32();
          this.duration = bits.read_bits(64); // TODO: loses precision
  // @version is 0
        } else {
          this.creationTime = bits.read_bits(32);
          this.creationDate = Box.mp4TimeToDate(this.creationTime);
          this.modificationTime = bits.read_bits(32);
          this.modificationDate = Box.mp4TimeToDate(this.modificationTime);
          this.timescale = bits.read_uint32();
          this.duration = bits.read_uint32();
        }
        this.durationSeconds = this.duration / this.timescale;
        pad = bits.read_bit();
        if (pad !== 0) {
          throw new Error(`mdhd: pad is not 0: ${pad}`);
        }
        this.language = Box.readLanguageCode(bits);
        pre_defined = bits.read_bits(16);
        if (pre_defined !== 0) {
          throw new Error(`mdhd: pre_defined is not 0: ${pre_defined}`);
        }
      }
  
      getDetails(detailLevel) {
        return `created=${formatDate(this.creationDate)} modified=${formatDate(this.modificationDate)} timescale=${this.timescale} durationSeconds=${this.durationSeconds} lang=${this.language}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.creationDate = this.creationDate;
        obj.modificationDate = this.modificationDate;
        obj.timescale = this.timescale;
        obj.duration = this.duration;
        obj.durationSeconds = this.durationSeconds;
        obj.language = this.language;
        return obj;
      }
  
    };
  
    // Handler Reference Box (hdlr): declares the nature of the media in a track
    // Container: Media Box ('mdia') or Meta Box ('meta')
    // Defined in ISO 14496-12
    HandlerBox = class HandlerBox extends Box {
      read(buf) {
        var bits, pre_defined;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        pre_defined = bits.read_bits(32);
        if (pre_defined !== 0) {
          throw new Error(`hdlr: pre_defined is not 0 (got ${pre_defined})`);
        }
        this.handlerType = bits.read_bytes(4).toString('utf8');
        // vide: Video track
        // soun: Audio track
        // hint: Hint track
        bits.skip_bytes(4 * 3); // reserved 0 bits (may not be all zero if handlerType is
        // none of the above)
        this.name = bits.get_string();
      }
  
      getDetails(detailLevel) {
        return `handlerType=${this.handlerType} name=${this.name}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.handlerType = this.handlerType;
        obj.name = this.name;
        return obj;
      }
  
    };
  
    // Video Media Header Box (vmhd): general presentation information
    VideoMediaHeaderBox = class VideoMediaHeaderBox extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.graphicsmode = bits.read_bits(16);
        if (this.graphicsmode !== 0) {
          logger.warn(`[mp4] warning: vmhd: non-standard graphicsmode: ${this.graphicsmode}`);
        }
        this.opcolor = {};
        this.opcolor.red = bits.read_bits(16);
        this.opcolor.green = bits.read_bits(16);
        return this.opcolor.blue = bits.read_bits(16);
      }
  
    };
  
    //  getDetails: (detailLevel) ->
    //    "graphicsMode=#{@graphicsmode} opColor=0x#{Bits.zeropad 2, @opcolor.red.toString 16}#{Bits.zeropad 2, @opcolor.green.toString 16}#{Bits.zeropad 2, @opcolor.blue.toString 16}"
  
    // Data Reference Box (dref): table of data references that declare
    //                            locations of the media data
    DataReferenceBox = class DataReferenceBox extends Box {
      read(buf) {
        var bits, entry_count, i, k, ref;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        entry_count = bits.read_uint32();
        this.children = [];
        for (i = k = 1, ref = entry_count; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
          this.children.push(Box.parse(bits, this));
        }
      }
  
    };
  
    // "url "
    DataEntryUrlBox = class DataEntryUrlBox extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        if (bits.has_more_data()) {
          this.location = bits.get_string();
        } else {
          this.location = null;
        }
      }
  
      getDetails(detailLevel) {
        if (this.location != null) {
          return `location=${this.location}`;
        } else {
          return "empty location value";
        }
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.location = this.location;
        return obj;
      }
  
    };
  
    // "urn "
    DataEntryUrnBox = class DataEntryUrnBox extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        if (bits.has_more_data()) {
          this.name = bits.get_string();
        } else {
          this.name = null;
        }
        if (bits.has_more_data()) {
          this.location = bits.get_string();
        } else {
          this.location = null;
        }
      }
  
    };
  
    // Sample Description Box (stsd): coding type and any initialization information
    // Defined in ISO 14496-12
    SampleDescriptionBox = class SampleDescriptionBox extends Box {
      read(buf) {
        var bits, entry_count, handlerRefBox, handlerType, i, k, ref;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        // moov
        //   mvhd
        //   iods
        //   trak
        //     tkhd
        //     edts
        //       elst
        //     mdia
        //       mdhd
        //       hdlr <- find target
        //       minf
        //         vmhd
        //         dinf
        //           dref
        //             url
        //         stbl
        //           stsd <- self
        //             stts
        handlerRefBox = this.findParent('mdia').find('hdlr');
        handlerType = handlerRefBox.handlerType;
        entry_count = bits.read_uint32();
        this.children = [];
        for (i = k = 1, ref = entry_count; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
          switch (handlerType) {
            case 'soun': // for audio tracks
              this.children.push(Box.parse(bits, this, AudioSampleEntry));
              break;
            case 'vide': // for video tracks
              this.children.push(Box.parse(bits, this, VisualSampleEntry));
              break;
            case 'hint': // hint track
              this.children.push(Box.parse(bits, this, HintSampleEntry));
              break;
            default:
              logger.warn(`[mp4] warning: ignoring a sample entry for unknown handlerType in stsd box: ${handlerType}`);
          }
        }
      }
  
    };
  
    HintSampleEntry = class HintSampleEntry extends Box {
      read(buf) {
        var bits, reserved;
        bits = new Bits(buf);
        // SampleEntry
        reserved = bits.read_bits(8 * 6);
        if (reserved !== 0) {
          throw new Error(`VisualSampleEntry: reserved bits are not 0: ${reserved}`);
        }
        this.dataReferenceIndex = bits.read_bits(16);
      }
  
    };
  
    // unsigned int(8) data []
    AudioSampleEntry = class AudioSampleEntry extends Box {
      read(buf) {
        var bits, mdhdBox, pre_defined, reserved;
        bits = new Bits(buf);
        // SampleEntry
        reserved = bits.read_bits(8 * 6);
        if (reserved !== 0) {
          throw new Error(`AudioSampleEntry: reserved bits are not 0: ${reserved}`);
        }
        this.dataReferenceIndex = bits.read_bits(16);
        reserved = bits.read_bytes_sum(4 * 2);
        if (reserved !== 0) {
          throw new Error(`AudioSampleEntry: reserved-1 bits are not 0: ${reserved}`);
        }
        this.channelCount = bits.read_bits(16);
        if (this.channelCount !== 2) {
          throw new Error(`AudioSampleEntry: channelCount is not 2: ${this.channelCount}`);
        }
        this.sampleSize = bits.read_bits(16);
        if (this.sampleSize !== 16) {
          throw new Error(`AudioSampleEntry: sampleSize is not 16: ${this.sampleSize}`);
        }
        pre_defined = bits.read_bits(16);
        if (pre_defined !== 0) {
          throw new Error(`AudioSampleEntry: pre_defined is not 0: ${pre_defined}`);
        }
        reserved = bits.read_bits(16);
        if (reserved !== 0) {
          throw new Error(`AudioSampleEntry: reserved-2 bits are not 0: ${reserved}`);
        }
        // moov
        //   mvhd
        //   iods
        //   trak
        //     tkhd
        //     edts
        //       elst
        //     mdia
        //       mdhd <- find target
        //       hdlr
        //       minf
        //         vmhd
        //         dinf
        //           dref
        //             url
        //         stbl
        //           stsd <- self
        //             stts
        mdhdBox = this.findParent('mdia').find('mdhd');
        this.sampleRate = bits.read_uint32();
        if (this.sampleRate !== mdhdBox.timescale * Math.pow(2, 16)) { // "<< 16" may lead to int32 overflow
          throw new Error(`AudioSampleEntry: illegal sampleRate: ${this.sampleRate} (should be ${mdhdBox.timescale << 16})`);
        }
        this.remaining_buf = bits.remaining_buffer();
      }
  
    };
  
    VisualSampleEntry = class VisualSampleEntry extends Box {
      read(buf) {
        var bits, compressorNameBytes, paddingLen, pre_defined, reserved;
        bits = new Bits(buf);
        // SampleEntry
        reserved = bits.read_bits(8 * 6);
        if (reserved !== 0) {
          throw new Error(`VisualSampleEntry: reserved bits are not 0: ${reserved}`);
        }
        this.dataReferenceIndex = bits.read_bits(16);
        // VisualSampleEntry
        pre_defined = bits.read_bits(16);
        if (pre_defined !== 0) {
          throw new Error(`VisualSampleEntry: pre_defined bits are not 0: ${pre_defined}`);
        }
        reserved = bits.read_bits(16);
        if (reserved !== 0) {
          throw new Error(`VisualSampleEntry: reserved bits are not 0: ${reserved}`);
        }
        pre_defined = bits.read_bytes_sum(4 * 3);
        if (pre_defined !== 0) {
          throw new Error(`VisualSampleEntry: pre_defined is not 0: ${pre_defined}`);
        }
        this.width = bits.read_bits(16);
        this.height = bits.read_bits(16);
        this.horizontalResolution = bits.read_uint32();
        if (this.horizontalResolution !== 0x00480000) { // 72 dpi
          throw new Error(`VisualSampleEntry: horizontalResolution is not 0x00480000: ${this.horizontalResolution}`);
        }
        this.verticalResolution = bits.read_uint32();
        if (this.verticalResolution !== 0x00480000) { // 72 dpi
          throw new Error(`VisualSampleEntry: verticalResolution is not 0x00480000: ${this.verticalResolution}`);
        }
        reserved = bits.read_uint32();
        if (reserved !== 0) {
          throw new Error(`VisualSampleEntry: reserved bits are not 0: ${reserved}`);
        }
        this.frameCount = bits.read_bits(16);
        if (this.frameCount !== 1) {
          throw new Error(`VisualSampleEntry: frameCount is not 1: ${this.frameCount}`);
        }
        // compressor name: 32 bytes
        compressorNameBytes = bits.read_byte();
        if (compressorNameBytes > 0) {
          this.compressorName = bits.read_bytes(compressorNameBytes).toString('utf8');
        } else {
          this.compressorName = null;
        }
        paddingLen = 32 - 1 - compressorNameBytes;
        if (paddingLen > 0) {
          bits.skip_bytes(paddingLen);
        }
        this.depth = bits.read_bits(16);
        if (this.depth !== 0x0018) {
          throw new Error(`VisualSampleEntry: depth is not 0x0018: ${this.depth}`);
        }
        pre_defined = bits.read_int(16);
        if (pre_defined !== -1) {
          throw new Error(`VisualSampleEntry: pre_defined is not -1: ${pre_defined}`);
        }
        this.remaining_buf = bits.remaining_buffer();
      }
  
    };
  
    // stts
    // Defined in ISO 14496-12
    TimeToSampleBox = class TimeToSampleBox extends Box {
      read(buf) {
        var bits, i, k, ref, sampleCount, sampleDelta;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.entryCount = bits.read_uint32();
        this.entries = [];
        for (i = k = 0, ref = this.entryCount; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          // number of consecutive samples that have the given duration
          sampleCount = bits.read_uint32();
          // delta of these samples in the time-scale of the media
          sampleDelta = bits.read_uint32();
          if (sampleDelta < 0) {
            throw new Error(`stts: negative sampleDelta is not allowed: ${sampleDelta}`);
          }
          this.entries.push({
            sampleCount: sampleCount,
            sampleDelta: sampleDelta
          });
        }
      }
  
      getTotalSamples() {
        var entry, k, len1, ref, samples;
        samples = 0;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          samples += entry.sampleCount;
        }
        return samples;
      }
  
      // Returns the total length of this media in seconds
      getTotalLength() {
        var entry, k, len1, mdhdBox, ref, time;
        // mdia
        //   mdhd <- find target
        //   hdlr
        //   minf
        //     vmhd
        //     dinf
        //       dref
        //         url
        //     stbl
        //       stsd
        //         avc1
        //           avcC
        //           btrt
        //       stts <- self
        mdhdBox = this.findParent('mdia').find('mdhd');
        time = 0;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          time += entry.sampleDelta * entry.sampleCount;
        }
        return time / mdhdBox.timescale;
      }
  
      // Returns a sample which comes exactly at or immediately after
      // the specified time (in seconds). If isExclusive=true, it excludes
      // a sample whose timestamp is equal to the specified time.
      // If there is no matching sample, this method returns null.
      getSampleAfterSeconds(sec, isExclusive = false) {
        var elstBox, entry, entryDuration, k, len1, numSamples, ref, ref1, remainingTime, sampleNumber, timescale, totalSeconds, totalTime;
        timescale = this.findParent('mdia').find('mdhd').timescale;
        remainingTime = sec * timescale;
        sampleNumber = 1;
        elstBox = (ref = this.findParent('trak').child('edts')) != null ? ref.child('elst') : void 0;
        if (elstBox != null) {
          totalTime = elstBox.getEmptyDuration();
          remainingTime -= totalTime;
        } else {
          totalTime = 0;
        }
        ref1 = this.entries;
        for (k = 0, len1 = ref1.length; k < len1; k++) {
          entry = ref1[k];
          numSamples = Math.ceil(remainingTime / entry.sampleDelta);
          if (numSamples < 0) {
            numSamples = 0;
          }
          if (numSamples <= entry.sampleCount) {
            totalTime += numSamples * entry.sampleDelta;
            totalSeconds = totalTime / timescale;
            if (isExclusive && (totalSeconds <= sec)) {
              numSamples++;
              totalTime += entry.sampleDelta;
              totalSeconds = totalTime / timescale;
            }
            return {
              sampleNumber: sampleNumber + numSamples,
              time: totalTime,
              seconds: totalSeconds
            };
          }
          sampleNumber += entry.sampleCount;
          entryDuration = entry.sampleDelta * entry.sampleCount;
          totalTime += entryDuration;
          remainingTime -= entryDuration;
        }
        // EOF
        return null;
      }
  
      // Returns a sample which represents the data at the specified time
      // (in seconds). If there is no sample at the specified time, this
      // method returns null.
      getSampleAtSeconds(sec) {
        var elstBox, entry, entryDuration, k, len1, ref, ref1, remainingTime, sampleIndexInChunk, sampleNumber, timescale, totalTime;
        timescale = this.findParent('mdia').find('mdhd').timescale;
        remainingTime = sec * timescale;
        sampleNumber = 1;
        elstBox = (ref = this.findParent('trak').child('edts')) != null ? ref.child('elst') : void 0;
        if (elstBox != null) {
          totalTime = elstBox.getEmptyDuration();
          remainingTime -= totalTime;
        } else {
          totalTime = 0;
        }
        ref1 = this.entries;
        for (k = 0, len1 = ref1.length; k < len1; k++) {
          entry = ref1[k];
          sampleIndexInChunk = Math.floor(remainingTime / entry.sampleDelta);
          if (sampleIndexInChunk < 0) {
            sampleIndexInChunk = 0;
          }
          if (sampleIndexInChunk < entry.sampleCount) {
            totalTime += sampleIndexInChunk * entry.sampleDelta;
            return {
              sampleNumber: sampleNumber + sampleIndexInChunk,
              time: totalTime,
              seconds: totalTime / timescale
            };
          }
          sampleNumber += entry.sampleCount;
          entryDuration = entry.sampleDelta * entry.sampleCount;
          totalTime += entryDuration;
          remainingTime -= entryDuration;
        }
        // EOF
        return null;
      }
  
      // Returns a decoding time for the given sample number.
      // The sample number starts at 1.
      getDecodingTime(sampleNumber) {
        var elstBox, entry, k, len1, mdhdBox, ref, ref1, time, trakBox;
        trakBox = this.findParent('trak');
        elstBox = (ref = trakBox.child('edts')) != null ? ref.child('elst') : void 0;
        mdhdBox = trakBox.child('mdia').child('mdhd');
        sampleNumber--;
        if (elstBox != null) {
          time = elstBox.getEmptyDuration();
        } else {
          time = 0;
        }
        ref1 = this.entries;
        for (k = 0, len1 = ref1.length; k < len1; k++) {
          entry = ref1[k];
          if (sampleNumber > entry.sampleCount) {
            time += entry.sampleDelta * entry.sampleCount;
            sampleNumber -= entry.sampleCount;
          } else {
            time += entry.sampleDelta * sampleNumber;
            break;
          }
        }
        return {
          time: time,
          seconds: time / mdhdBox.timescale
        };
      }
  
      getDetails(detailLevel) {
        var str;
        str = `entryCount=${this.entryCount}`;
        if (detailLevel >= 2) {
          str += ' ' + this.entries.map(function(entry, index) {
            return `[${index}]:sampleCount=${entry.sampleCount},sampleDelta=${entry.sampleDelta}`;
          }).join(',');
        }
        return str;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.entries = this.entries;
        return obj;
      }
  
    };
  
    // stss: random access points
    // If stss is not present, every sample is a random access point.
    SyncSampleBox = class SyncSampleBox extends Box {
      read(buf) {
        var bits, i, k, lastSampleNumber, ref, sampleNumber;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.entryCount = bits.read_uint32();
        this.sampleNumbers = [];
        lastSampleNumber = -1;
        for (i = k = 0, ref = this.entryCount; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          sampleNumber = bits.read_uint32();
          if (sampleNumber < lastSampleNumber) {
            throw new Error(`stss: sample number must be in increasing order: ${sampleNumber} < ${lastSampleNumber}`);
          }
          lastSampleNumber = sampleNumber;
          this.sampleNumbers.push(sampleNumber);
        }
      }
  
      getDetails(detailLevel) {
        if (detailLevel >= 2) {
          return `sampleNumbers=${this.sampleNumbers.join(',')}`;
        } else {
          return `entryCount=${this.entryCount}`;
        }
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.sampleNumbers = this.sampleNumbers;
        return obj;
      }
  
    };
  
    // stsc: number of samples for each chunk
    SampleToChunkBox = class SampleToChunkBox extends Box {
      read(buf) {
        var bits, endIndex, firstChunk, i, k, l, lastEntry, ref, ref1, sampleDescriptionIndex, sampleNumber, samplesPerChunk;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.entryCount = bits.read_uint32();
        this.entries = [];
        sampleNumber = 1;
        for (i = k = 0, ref = this.entryCount; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          firstChunk = bits.read_uint32();
          samplesPerChunk = bits.read_uint32();
          sampleDescriptionIndex = bits.read_uint32();
          if (i > 0) {
            lastEntry = this.entries[this.entries.length - 1];
            sampleNumber += (firstChunk - lastEntry.firstChunk) * lastEntry.samplesPerChunk;
          }
          this.entries.push({
            firstChunk: firstChunk,
            firstSample: sampleNumber,
            samplesPerChunk: samplesPerChunk,
            sampleDescriptionIndex: sampleDescriptionIndex
          });
        }
        // Determine the number of chunks of each entry
        endIndex = this.entries.length - 1;
        for (i = l = 0, ref1 = endIndex; (0 <= ref1 ? l < ref1 : l > ref1); i = 0 <= ref1 ? ++l : --l) {
          if (i === endIndex) {
            break;
          }
          this.entries[i].numChunks = this.entries[i + 1].firstChunk - this.entries[i].firstChunk;
        }
      }
  
      // XXX: We could determine the number of chunks in the last batch because
      //      the total number of samples is known by stts box. However we don't
      //      need it.
      getNumSamplesExceptLastChunk() {
        var entry, k, len1, ref, samples;
        samples = 0;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          if (entry.numChunks != null) {
            samples += entry.samplesPerChunk * entry.numChunks;
          }
        }
        return samples;
      }
  
      getNumSamplesInChunk(chunk) {
        var entry, k, len1, ref, sttsBox;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          if (entry.numChunks == null) {
            // TOOD: too heavy
            sttsBox = this.findParent('stbl').find('stts');
            return entry.samplesPerChunk;
          }
          if (chunk < entry.firstChunk + entry.numChunks) {
            return entry.samplesPerChunk;
          }
        }
        throw new Error(`Chunk not found: ${chunk}`);
      }
  
      findChunk(sampleNumber) {
        var entry, k, len1, ref;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          if (entry.numChunks == null) {
            return entry.firstChunk + Math.floor((sampleNumber - 1) / entry.samplesPerChunk);
          }
          if (sampleNumber <= entry.samplesPerChunk * entry.numChunks) {
            return entry.firstChunk + Math.floor((sampleNumber - 1) / entry.samplesPerChunk);
          }
          sampleNumber -= entry.samplesPerChunk * entry.numChunks;
        }
        throw new Error(`Chunk for sample number ${sampleNumber} is not found`);
      }
  
      getFirstSampleNumberInChunk(chunkNumber) {
        var i, k, ref;
        for (i = k = ref = this.entries.length - 1; (ref <= 0 ? k <= 0 : k >= 0); i = ref <= 0 ? ++k : --k) {
          if (chunkNumber >= this.entries[i].firstChunk) {
            return this.entries[i].firstSample + (chunkNumber - this.entries[i].firstChunk) * this.entries[i].samplesPerChunk;
          }
        }
        return null;
      }
  
      getDetails(detailLevel) {
        if (detailLevel >= 2) {
          return this.entries.map(function(entry) {
            return `firstChunk=${entry.firstChunk} samplesPerChunk=${entry.samplesPerChunk} sampleDescriptionIndex=${entry.sampleDescriptionIndex}`;
          }).join(', ');
        } else {
          return `entryCount=${this.entryCount}`;
        }
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.entries = this.entries;
        return obj;
      }
  
    };
  
    // stsz: sample sizes
    SampleSizeBox = class SampleSizeBox extends Box {
      read(buf) {
        var bits, i, k, ref;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        // Default sample size
        // 0: samples have different sizes
        this.sampleSize = bits.read_uint32();
        // Number of samples in the track
        this.sampleCount = bits.read_uint32();
        if (this.sampleSize === 0) {
          this.entrySizes = [];
          for (i = k = 1, ref = this.sampleCount; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
            this.entrySizes.push(bits.read_uint32());
          }
        }
      }
  
      // Returns an array of sample sizes beginning at sampleNumber through
      // the specified number of samples (len)
      getSampleSizes(sampleNumber, len = 1) {
        var i, k, l, ref, ref1, sizes;
        sizes = [];
        if (this.sampleSize !== 0) {
          for (i = k = ref = len; (ref <= 0 ? k < 0 : k > 0); i = ref <= 0 ? ++k : --k) {
            sizes.push(this.sampleSize);
          }
        } else {
          for (i = l = ref1 = len; (ref1 <= 0 ? l < 0 : l > 0); i = ref1 <= 0 ? ++l : --l) {
            sizes.push(this.entrySizes[sampleNumber - 1]);
            sampleNumber++;
          }
        }
        return sizes;
      }
  
      // Returns the total bytes from sampleNumber through
      // the specified number of samples (len)
      getTotalSampleSize(sampleNumber, len = 1) {
        var i, k, ref, totalLength;
        if (this.sampleSize !== 0) { // all samples are the same size
          return this.sampleSize * len; // the samples have different sizes
        } else {
          totalLength = 0;
          for (i = k = ref = len; (ref <= 0 ? k < 0 : k > 0); i = ref <= 0 ? ++k : --k) {
            if (sampleNumber > this.entrySizes.length) {
              throw new Error(`Sample number is out of range: ${sampleNumber} > ${this.entrySizes.length}`);
            }
            totalLength += this.entrySizes[sampleNumber - 1];
            sampleNumber++;
          }
          return totalLength;
        }
      }
  
      getDetails(detailLevel) {
        var str;
        str = `sampleSize=${this.sampleSize} sampleCount=${this.sampleCount}`;
        if (this.entrySizes != null) {
          if (detailLevel >= 2) {
            str += ` entrySizes=${this.entrySizes.join(',')}`;
          } else {
            str += ` num_entrySizes=${this.entrySizes.length}`;
          }
        }
        return str;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.sampleSize = this.sampleSize;
        obj.sampleCount = this.sampleCount;
        if (this.entrySizes != null) {
          obj.entrySizes = this.entrySizes;
        }
        return obj;
      }
  
    };
  
    // stco: chunk offsets relative to the beginning of the file
    ChunkOffsetBox = class ChunkOffsetBox extends Box {
      read(buf) {
        var bits, i, k, ref;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.entryCount = bits.read_uint32();
        this.chunkOffsets = [];
        for (i = k = 1, ref = this.entryCount; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
          this.chunkOffsets.push(bits.read_uint32());
        }
      }
  
      // Returns a position of the chunk relative to the beginning of the file
      getChunkOffset(chunkNumber) {
        if ((chunkNumber <= 0) || (chunkNumber > this.chunkOffsets.length)) {
          throw new Error(`Chunk number out of range: ${chunkNumber} (len=${this.chunkOffsets.length})`);
        }
        return this.chunkOffsets[chunkNumber - 1];
      }
  
      getDetails(detailLevel) {
        if (detailLevel >= 2) {
          return `chunkOffsets=${this.chunkOffsets.join(',')}`;
        } else {
          return `entryCount=${this.entryCount}`;
        }
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.chunkOffsets = this.chunkOffsets;
        return obj;
      }
  
    };
  
    // smhd
    SoundMediaHeaderBox = class SoundMediaHeaderBox extends Box {
      read(buf) {
        var bits, reserved;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.balance = bits.read_bits(16);
        if (this.balance !== 0) {
          throw new Error(`smhd: balance is not 0: ${this.balance}`);
        }
        reserved = bits.read_bits(16);
        if (reserved !== 0) {
          throw new Error(`smhd: reserved bits are not 0: ${reserved}`);
        }
      }
  
    };
  
    // meta: descriptive or annotative metadata
    MetaBox = class MetaBox extends Box {
      read(buf) {
        var bits, box;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.children = [];
        while (bits.has_more_data()) {
          box = Box.parse(bits, this);
          this.children.push(box);
        }
      }
  
    };
  
    // pitm: one of the referenced items
    PrimaryItemBox = class PrimaryItemBox extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.itemID = bits.read_bits(16);
      }
  
    };
  
    // iloc
    ItemLocationBox = class ItemLocationBox extends Box {
      read(buf) {
        var baseOffset, bits, dataReferenceIndex, extentCount, extentLength, extentOffset, extents, i, itemID, j, k, l, ref, ref1;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.offsetSize = bits.read_bits(4);
        this.lengthSize = bits.read_bits(4);
        this.baseOffsetSize = bits.read_bits(4);
        this.reserved = bits.read_bits(4);
        this.itemCount = bits.read_bits(16);
        this.items = [];
        for (i = k = 0, ref = this.itemCount; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          itemID = bits.read_bits(16);
          dataReferenceIndex = bits.read_bits(16);
          baseOffset = bits.read_bits(this.baseOffsetSize * 8);
          extentCount = bits.read_bits(16);
          extents = [];
          for (j = l = 0, ref1 = extentCount; (0 <= ref1 ? l < ref1 : l > ref1); j = 0 <= ref1 ? ++l : --l) {
            extentOffset = bits.read_bits(this.offsetSize * 8);
            extentLength = bits.read_bits(this.lengthSize * 8);
            extents.push({
              extentOffset: extentOffset,
              extentLength: extentLength
            });
          }
          this.items.push({
            itemID: itemID,
            dataReferenceIndex: dataReferenceIndex,
            baseOffset: baseOffset,
            extentCount: extentCount,
            extents: extents
          });
        }
      }
  
    };
  
    // ipro: an array of item protection information
    ItemProtectionBox = class ItemProtectionBox extends Box {
      read(buf) {
        var bits, box, i, k, ref;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.protectionCount = bits.read_bits(16);
        this.children = [];
        for (i = k = 1, ref = this.protectionCount; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
          box = Box.parse(bits, this);
          this.children.push(box);
        }
      }
  
    };
  
    // infe: extra information about selected items
    ItemInfoEntry = class ItemInfoEntry extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.itemID = bits.read_bits(16);
        this.itemProtectionIndex = bits.read_bits(16);
        this.itemName = bits.get_string();
        this.contentType = bits.get_string();
        if (bits.has_more_data()) {
          this.contentEncoding = bits.get_string();
        }
      }
  
    };
  
    // iinf: extra information about selected items
    ItemInfoBox = class ItemInfoBox extends Box {
      read(buf) {
        var bits, box, i, k, ref;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.entryCount = bits.read_bits(16);
        this.children = [];
        for (i = k = 1, ref = this.entryCount; (1 <= ref ? k <= ref : k >= ref); i = 1 <= ref ? ++k : --k) {
          box = Box.parse(bits, this);
          this.children.push(box);
        }
      }
  
    };
  
    // ilst: list of actual metadata values
    MetadataItemListBox = class MetadataItemListBox extends Container {};
  
    GenericDataBox = class GenericDataBox extends Box {
      read(buf) {
        var bits, nullPos, zeroBytes;
        bits = new Bits(buf);
        this.length = bits.read_uint32();
        this.name = bits.read_bytes(4).toString('utf8');
        this.entryCount = bits.read_uint32();
        zeroBytes = bits.read_bytes_sum(4);
        if (zeroBytes !== 0) {
          logger.warn(`[mp4] warning: zeroBytes are not all zeros (got ${zeroBytes})`);
        }
        this.value = bits.read_bytes(this.length - 16);
        nullPos = Bits.searchByteInBuffer(this.value, 0x00);
        if (nullPos === 0) {
          this.valueStr = null;
        } else if (nullPos !== -1) {
          this.valueStr = this.value.slice(0, nullPos).toString('utf8');
        } else {
          this.valueStr = this.value.toString('utf8');
        }
      }
  
      getDetails(detailLevel) {
        return `${this.name}=${this.valueStr}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.data = this.valueStr;
        return obj;
      }
  
    };
  
    // gsst: unknown
    GoogleGSSTBox = class GoogleGSSTBox extends GenericDataBox {};
  
    // gstd: unknown
    GoogleGSTDBox = class GoogleGSTDBox extends GenericDataBox {};
  
    // gssd: unknown
    GoogleGSSDBox = class GoogleGSSDBox extends GenericDataBox {};
  
    // gspu: unknown
    GoogleGSPUBox = class GoogleGSPUBox extends GenericDataBox {};
  
    // gspm: unknown
    GoogleGSPMBox = class GoogleGSPMBox extends GenericDataBox {};
  
    // gshh: unknown
    GoogleGSHHBox = class GoogleGSHHBox extends GenericDataBox {};
  
    // Media Data Box (mdat): audio/video frames
    // Defined in ISO 14496-12
    MediaDataBox = class MediaDataBox extends Box {
      read(buf) {}
  
    };
  
    // We will not parse the raw media stream
  
    // avc1
    // Defined in ISO 14496-15
    AVCSampleEntry = class AVCSampleEntry extends VisualSampleEntry {
      read(buf) {
        var bits;
        super.read(buf);
        bits = new Bits(this.remaining_buf);
        this.children = [];
        this.children.push(Box.parse(bits, this, AVCConfigurationBox));
        if (bits.has_more_data()) {
          this.children.push(Box.parse(bits, this, MPEG4BitRateBox));
        }
        if (bits.has_more_data()) {
          this.children.push(Box.parse(bits, this, MPEG4ExtensionDescriptorsBox));
        }
      }
  
    };
  
    // btrt
    // Defined in ISO 14496-15
    MPEG4BitRateBox = class MPEG4BitRateBox extends Box {
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.bufferSizeDB = bits.read_uint32();
        this.maxBitrate = bits.read_uint32();
        return this.avgBitrate = bits.read_uint32();
      }
  
      getDetails(detailLevel) {
        return `bufferSizeDB=${this.bufferSizeDB} maxBitrate=${this.maxBitrate} avgBitrate=${this.avgBitrate}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.bufferSizeDB = this.bufferSizeDB;
        obj.maxBitrate = this.maxBitrate;
        obj.avgBitrate = this.avgBitrate;
        return obj;
      }
  
    };
  
    // m4ds
    // Defined in ISO 14496-15
    MPEG4ExtensionDescriptorsBox = class MPEG4ExtensionDescriptorsBox {
      read(buf) {}
  
    };
  
    // TODO: implement this?
  
    // avcC
    // Defined in ISO 14496-15
    AVCConfigurationBox = class AVCConfigurationBox extends Box {
      read(buf) {
        var bits, i, k, l, pictureParameterSetLength, ref, ref1, reserved, sequenceParameterSetLength;
        bits = new Bits(buf);
        // AVCDecoderConfigurationRecord
        this.configurationVersion = bits.read_byte();
        if (this.configurationVersion !== 1) {
          logger.warn(`warning: mp4: avcC: unknown configurationVersion: ${this.configurationVersion}`);
        }
        this.AVCProfileIndication = bits.read_byte();
        this.profileCompatibility = bits.read_byte();
        this.AVCLevelIndication = bits.read_byte();
        reserved = bits.read_bits(6);
        //    if reserved isnt 0b111111  # XXX: not always 0b111111?
        //      throw new Error "AVCConfigurationBox: reserved-1 is not #{0b111111} (got #{reserved})"
        this.lengthSizeMinusOne = bits.read_bits(2);
        reserved = bits.read_bits(3);
        //    if reserved isnt 0b111  # XXX: not always 0b111?
        //      throw new Error "AVCConfigurationBox: reserved-2 is not #{0b111} (got #{reserved})"
  
        // SPS
        this.numOfSequenceParameterSets = bits.read_bits(5);
        this.sequenceParameterSets = [];
        for (i = k = 0, ref = this.numOfSequenceParameterSets; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          sequenceParameterSetLength = bits.read_bits(16);
          this.sequenceParameterSets.push(bits.read_bytes(sequenceParameterSetLength));
        }
        // PPS
        this.numOfPictureParameterSets = bits.read_byte();
        this.pictureParameterSets = [];
        for (i = l = 0, ref1 = this.numOfPictureParameterSets; (0 <= ref1 ? l < ref1 : l > ref1); i = 0 <= ref1 ? ++l : --l) {
          pictureParameterSetLength = bits.read_bits(16);
          this.pictureParameterSets.push(bits.read_bytes(pictureParameterSetLength));
        }
      }
  
      getDetails(detailLevel) {
        return 'sps=' + this.sequenceParameterSets.map(function(sps) {
          return `0x${sps.toString('hex')}`;
        }).join(',') + ' pps=' + this.pictureParameterSets.map(function(pps) {
          return `0x${pps.toString('hex')}`;
        }).join(',');
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.sps = this.sequenceParameterSets.map(function(sps) {
          return [...sps];
        });
        obj.pps = this.pictureParameterSets.map(function(pps) {
          return [...pps];
        });
        return obj;
      }
  
    };
  
    // esds
    ESDBox = class ESDBox extends Box {
      readDecoderConfigDescriptor(bits) {
        var info, reserved;
        info = {};
        info.tag = bits.read_byte();
        if (info.tag !== 0x04) { // 0x04 == DecoderConfigDescrTag
          throw new Error(`ESDBox: DecoderConfigDescrTag is not 4 (got ${info.tag})`);
        }
        info.length = this.readDescriptorLength(bits);
        info.objectProfileIndication = bits.read_byte();
        info.streamType = bits.read_bits(6);
        info.upStream = bits.read_bit();
        reserved = bits.read_bit();
        if (reserved !== 1) {
          throw new Error(`ESDBox: DecoderConfigDescriptor: reserved bit is not 1 (got ${reserved})`);
        }
        info.bufferSizeDB = bits.read_bits(24);
        info.maxBitrate = bits.read_uint32();
        info.avgBitrate = bits.read_uint32();
        info.decoderSpecificInfo = this.readDecoderSpecificInfo(bits);
        return info;
      }
  
      readDecoderSpecificInfo(bits) {
        var info;
        info = {};
        info.tag = bits.read_byte();
        if (info.tag !== 0x05) { // 0x05 == DecSpecificInfoTag
          throw new Error(`ESDBox: DecSpecificInfoTag is not 5 (got ${info.tag})`);
        }
        info.length = this.readDescriptorLength(bits);
        info.specificInfo = bits.read_bytes(info.length);
        return info;
      }
  
      readSLConfigDescriptor(bits) {
        var info, reserved;
        info = {};
        info.tag = bits.read_byte();
        if (info.tag !== 0x06) { // 0x06 == SLConfigDescrTag
          throw new Error(`ESDBox: SLConfigDescrTag is not 6 (got ${info.tag})`);
        }
        info.length = this.readDescriptorLength(bits);
        info.predefined = bits.read_byte();
        if (info.predefined === 0) {
          info.useAccessUnitStartFlag = bits.read_bit();
          info.useAccessUnitEndFlag = bits.read_bit();
          info.useRandomAccessPointFlag = bits.read_bit();
          info.hasRandomAccessUnitsOnlyFlag = bits.read_bit();
          info.usePaddingFlag = bits.read_bit();
          info.useTimeStampsFlag = bits.read_bit();
          info.useIdleFlag = bits.read_bit();
          info.durationFlag = bits.read_bit();
          info.timeStampResolution = bits.read_uint32();
          info.ocrResolution = bits.read_uint32();
          info.timeStampLength = bits.read_byte();
          if (info.timeStampLength > 64) {
            throw new Error(`ESDBox: SLConfigDescriptor: timeStampLength must be <= 64 (got ${info.timeStampLength})`);
          }
          info.ocrLength = bits.read_byte();
          if (info.ocrLength > 64) {
            throw new Error(`ESDBox: SLConfigDescriptor: ocrLength must be <= 64 (got ${info.ocrLength})`);
          }
          info.auLength = bits.read_byte();
          if (info.auLength > 32) {
            throw new Error(`ESDBox: SLConfigDescriptor: auLength must be <= 64 (got ${info.auLength})`);
          }
          info.instantBitrateLength = bits.read_byte();
          info.degradationPriorityLength = bits.read_bits(4);
          info.auSeqNumLength = bits.read_bits(5);
          if (info.auSeqNumLength > 16) {
            throw new Error(`ESDBox: SLConfigDescriptor: auSeqNumLength must be <= 16 (got ${info.auSeqNumLength})`);
          }
          info.packetSeqNumLength = bits.read_bits(5);
          if (info.packetSeqNumLength > 16) {
            throw new Error(`ESDBox: SLConfigDescriptor: packetSeqNumLength must be <= 16 (got ${info.packetSeqNumLength})`);
          }
          reserved = bits.read_bits(2);
          if (reserved !== 0b11) {
            throw new Error(`ESDBox: SLConfigDescriptor: reserved bits value is not ${0b11} (got ${reserved})`);
          }
          if (info.durationFlag === 1) {
            info.timeScale = bits.read_uint32();
            info.accessUnitDuration = bits.read_bits(16);
            info.compositionUnitDuration = bits.read_bits(16);
          }
          if (info.useTimeStampsFlag === 0) {
            info.startDecodingTimeStamp = bits.read_bits(info.timeStampLength);
            info.startCompositionTimeStamp = bits.read_bits(info.timeStamplength);
          }
        }
        return info;
      }
  
      readDescriptorLength(bits) {
        var len;
        len = bits.read_byte();
        // TODO: Is this correct?
        if (len >= 0x80) {
          len = ((len & 0x7f) << 21) | ((bits.read_byte() & 0x7f) << 14) | ((bits.read_byte() & 0x7f) << 7) | bits.read_byte();
        }
        return len;
      }
  
      read(buf) {
        var bits;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        // ES_Descriptor (defined in ISO 14496-1)
        this.tag = bits.read_byte();
        if (this.tag !== 0x03) { // 0x03 == ES_DescrTag
          throw new Error(`ESDBox: tag is not ${0x03} (got ${this.tag})`);
        }
        this.length = this.readDescriptorLength(bits);
        this.ES_ID = bits.read_bits(16);
        this.streamDependenceFlag = bits.read_bit();
        this.urlFlag = bits.read_bit();
        this.ocrStreamFlag = bits.read_bit();
        this.streamPriority = bits.read_bits(5);
        if (this.streamDependenceFlag === 1) {
          this.depenedsOnES_ID = bits.read_bits(16);
        }
        if (this.urlFlag === 1) {
          this.urlLength = bits.read_byte();
          this.urlString = bits.read_bytes(this.urlLength);
        }
        if (this.ocrStreamFlag === 1) {
          this.ocrES_ID = bits.read_bits(16);
        }
        this.decoderConfigDescriptor = this.readDecoderConfigDescriptor(bits);
        // TODO: if ODProfileLevelIndication is 0x01
        this.slConfigDescriptor = this.readSLConfigDescriptor(bits);
      }
  
      // TODO:
      // IPI_DescPointer
      // IP_IdentificationDataSet
      // IPMP_DescriptorPointer
      // LanguageDescriptor
      // QoS_Descriptor
      // RegistrationDescriptor
      // ExtensionDescriptor
      getDetails(detailLevel) {
        return `audioSpecificConfig=0x${this.decoderConfigDescriptor.decoderSpecificInfo.specificInfo.toString('hex')} maxBitrate=${this.decoderConfigDescriptor.maxBitrate} avgBitrate=${this.decoderConfigDescriptor.avgBitrate}`;
      }
  
      getTree() {
        var obj;
        obj = new Box;
        obj.audioSpecificConfig = [...this.decoderConfigDescriptor.decoderSpecificInfo.specificInfo];
        obj.maxBitrate = this.decoderConfigDescriptor.maxBitrate;
        obj.avgBitrate = this.decoderConfigDescriptor.avgBitrate;
        return obj;
      }
  
    };
  
    // mp4a
    // Defined in ISO 14496-14
    MP4AudioSampleEntry = class MP4AudioSampleEntry extends AudioSampleEntry {
      read(buf) {
        var bits;
        super.read(buf);
        bits = new Bits(this.remaining_buf);
        this.children = [Box.parse(bits, this, ESDBox)];
      }
  
    };
  
    // free: can be ignored
    // Defined in ISO 14496-12
    FreeSpaceBox = class FreeSpaceBox extends Box {};
  
    // ctts: offset between decoding time and composition time
    CompositionOffsetBox = class CompositionOffsetBox extends Box {
      read(buf) {
        var bits, i, k, ref, sampleCount, sampleOffset;
        bits = new Bits(buf);
        this.readFullBoxHeader(bits);
        this.entryCount = bits.read_uint32();
        this.entries = [];
        for (i = k = 0, ref = this.entryCount; (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) {
          sampleCount = bits.read_uint32();
          sampleOffset = bits.read_uint32();
          this.entries.push({
            sampleCount: sampleCount,
            sampleOffset: sampleOffset
          });
        }
      }
  
      // sampleNumber is indexed from 1
      getCompositionTimeOffset(sampleNumber) {
        var entry, k, len1, ref;
        ref = this.entries;
        for (k = 0, len1 = ref.length; k < len1; k++) {
          entry = ref[k];
          if (sampleNumber <= entry.sampleCount) {
            return entry.sampleOffset;
          }
          sampleNumber -= entry.sampleCount;
        }
        throw new Error(`mp4: ctts: composition time for sample number ${sampleNumber} not found`);
      }
  
    };
  
    // '\xa9too' (copyright sign + 'too')
    CTOOBox = class CTOOBox extends GenericDataBox {};
  
    api = {
      MP4File: MP4File
    };
  
    module.exports = api;
  
  }).call(this);
  