     @tree = { boxes: [] }
     while @bits.has_more_data()
       box = Box.parse @bits, null  # null == root box
-      process.stdout.write box.dump 0, 1
+      if box instanceof MovieBox
+        @moovBox = box
+      else if box instanceof MediaDataBox
+        @mdatBox = box
+#      process.stdout.write box.dump 0, 1
       @tree.boxes.push box.getTree()
-      if box instanceof MediaDataBox  # found mdat
-        console.log box
     diffTime = process.hrtime startTime
     console.log "took #{(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to parse"
     console.log "EOF"
-    return
+
+    for child in @moovBox.children
+      if child instanceof TrackBox  # trak
+        tkhdBox = child.find 'tkhd'
+        if tkhdBox.isAudioTrack
+          @audioTrakBox = child
+        else
+          @videoTrakBox = child
+
+    return
+
+  getSPS: ->
+    avcCBox = @videoTrakBox.find 'avcC'
+    return avcCBox.sequenceParameterSets[0]
+
+  getPPS: ->
+    avcCBox = @videoTrakBox.find 'avcC'
+    return avcCBox.pictureParameterSets[0]
+
+  getAudioSpecificConfig: ->
+    esdsBox = @audioTrakBox.find 'esds'
+    return esdsBox.decoderConfigDescriptor.decoderSpecificInfo.specificInfo
+
+  play: ->
+    @currentTime = 0
+    @consumedAudioSamples = 0
+    @consumedVideoSamples = 0
+    @consumedAudioChunks = 0
+    @consumedVideoChunks = 0
+    startTime = process.hrtime()
+    @numVideoSamples = @getNumVideoSamples()
+    @numAudioSamples = @getNumAudioSamples()
+
+    seq = new Sequent
+
+    @bufferedAudioTime = 0
+    @bufferedVideoTime = 0
+    @queuedAudioTime = 0
+    @queuedVideoTime = 0
+    @currentPlayTime = 0
+    @playStartTime = getCurrentTime()
+    @bufferedAudioSamples = []
+    @bufferedVideoSamples = []
+
+    @isAudioEOF = false
+    @isVideoEOF = false
+
+    @bufferAudio =>
+      # audio samples has been buffered
+      seq.done()
+
+    @bufferVideo =>
+      # video samples has been buffered
+      seq.done()
+
+    seq.wait 2, =>
+      # ready to play
+      @startStreaming()
+
+  checkAudioBuffer: ->
+    timeDiff = @bufferedAudioTime - @currentPlayTime
+    if timeDiff < READ_BUFFER_TIME
+      # fill buffer
+      if @readNextAudioChunk()
+        @queueBufferedSamples()
+    else
+      @queueBufferedSamples()
+    return
+
+  checkVideoBuffer: ->
+    timeDiff = @bufferedVideoTime - @currentPlayTime
+    if timeDiff < READ_BUFFER_TIME
+      if @readNextVideoChunk()
+        @queueBufferedSamples()
+    else
+      @queueBufferedSamples()
+    return
+
+  startStreaming: ->
+    @queueBufferedSamples()
+
+  updateCurrentPlayTime: ->
+    @currentPlayTime = getCurrentTime() - @playStartTime
+
+  queueBufferedAudioSamples: ->
+    audioSample = @bufferedAudioSamples.shift()
+    if not audioSample?  # @bufferedAudioSamples is empty
+      return
+    timeDiff = audioSample.time - @currentPlayTime
+    if timeDiff <= MIN_TIME_DIFF
+      @emit 'audio_data', audioSample.data, audioSample.pts
+      @updateCurrentPlayTime()
+    else
+      setTimeout =>
+        @emit 'audio_data', audioSample.data, audioSample.pts
+        @updateCurrentPlayTime()
+        @checkAudioBuffer()
+      , timeDiff * 1000
+    @queuedAudioTime = audioSample.time
+    if @queuedAudioTime - @currentPlayTime < QUEUE_BUFFER_TIME
+      @queueBufferedSamples()
+
+  queueBufferedVideoSamples: ->
+    videoSample = @bufferedVideoSamples.shift()
+    if not videoSample?  # read buffer is empty
+      return
+    timeDiff = videoSample.time - @currentPlayTime
+    if timeDiff <= MIN_TIME_DIFF
+      console.log "emit1"
+      @emit 'video_data', videoSample.data, videoSample.pts
+      @updateCurrentPlayTime()
+    else
+      setTimeout =>
+        @emit 'video_data', videoSample.data, videoSample.pts
+        @updateCurrentPlayTime()
+        @checkVideoBuffer()
+      , timeDiff * 1000
+    @queuedVideoTime = videoSample.time
+    if @queuedVideoTime - @currentPlayTime < QUEUE_BUFFER_TIME
+      @queueBufferedSamples()
+
+  queueBufferedSamples: ->
+    @queueBufferedAudioSamples()
+    @queueBufferedVideoSamples()
+
+  bufferAudio: (callback) ->
+    # TODO: Use async
+    while @bufferedAudioTime < @currentPlayTime + READ_BUFFER_TIME
+      if not @readNextAudioChunk()
+        @isAudioEOF = true
+    callback?()
+
+  bufferVideo: (callback) ->
+    # TODO: Use async
+    while @bufferedVideoTime < @currentPlayTime + READ_BUFFER_TIME
+      if not @readNextVideoChunk()
+        @isVideoEOF = true
+    callback?()
+
+  getNumVideoSamples: ->
+    sttsBox = @videoTrakBox.find 'stts'
+    return sttsBox.getTotalSamples()
+
+  getNumAudioSamples: ->
+    sttsBox = @audioTrakBox.find 'stts'
+    return sttsBox.getTotalSamples()
+
+  parseH264Sample: (buf) ->
+    # The format is defined in ISO 14496-15 5.2.3
+
+    # Why do we need a length header here even we
+    # already have the same data in stsz box???
+    avcCBox = @videoTrakBox.find 'avcC'
+    lengthSize = avcCBox.lengthSizeMinusOne + 1
+    bits = new Bits buf
+    length = bits.read_bits lengthSize * 8
+    nalUnitType = bits.get_byte_at(0) & 0x1f
+    bits.skip_bytes length
+
+    if bits.get_remaining_bits() isnt 0
+      throw new Error "remaining bits is not zero: #{bits.get_remaining_bits()}"
+
+  readChunk: (chunkNumber, sampleNumber, trakBox) ->
+    sttsBox = trakBox.find 'stts'
+    stscBox = trakBox.find 'stsc'
+    numSamplesInChunk = stscBox.getNumSamplesInChunk chunkNumber
+    stcoBox = trakBox.find 'stco'
+    chunkOffset = stcoBox.getChunkOffset chunkNumber
+    stszBox = trakBox.find 'stsz'
+    sampleSizes = stszBox.getSampleSizes sampleNumber, numSamplesInChunk
+    chunkLength = stszBox.getSampleSize sampleNumber, numSamplesInChunk
+    samples = []
+    sampleOffset = 0
+    for sampleSize, i in sampleSizes
+      sampleTime = sttsBox.getDecodingTime sampleNumber + i
+      samples.push {
+        pts: sampleTime.pts
+        time: sampleTime.seconds
+        data: @fileBuf[chunkOffset+sampleOffset...chunkOffset+sampleOffset+sampleSize]
+      }
+      sampleOffset += sampleSize
+
+    return samples
+
+  readNextVideoChunk: ->
+    if @consumedVideoSamples >= @numVideoSamples
+      return false
+    nextChunkNumber = @consumedVideoChunks + 1
+    nextSampleNumber = @consumedVideoSamples + 1
+
+    samples = @readChunk nextChunkNumber, nextSampleNumber, @videoTrakBox
+
+    for sample in samples
+#      @parseH264Sample sample.data
+      # Remove length header
+      sample.data = sample.data[4..]
+
+    @consumedVideoChunks++
+    @consumedVideoSamples += samples.length
+    @bufferedVideoTime = samples[samples.length-1].time
+    @bufferedVideoSamples = @bufferedVideoSamples.concat samples
+
+    return true
+
+  parseAACSample: (buf) ->
+    # nop
+
+  readNextAudioChunk: ->
+    if @consumedAudioSamples >= @numAudioSamples
+      return false
+    nextChunkNumber = @consumedAudioChunks + 1
+    nextSampleNumber = @consumedAudioSamples + 1
+
+    samples = @readChunk nextChunkNumber, nextSampleNumber, @audioTrakBox
+
+#    for sample in samples
+#      @parseAACSample sample.data
+
+    @consumedAudioChunks++
+    @consumedAudioSamples += samples.length
+    @bufferedAudioTime = samples[samples.length-1].time
+    @bufferedAudioSamples = @bufferedAudioSamples.concat samples
+
+    return true
+
+EventEmitterModule.mixin MP4File
 
 class Box
   # time: seconds since midnight, Jan. 1, 1904 UTC

# (copyright sign) + 'too'
class CTOOBox extends GenericDataBox

if process.argv.length < 3
  console.log "Error: specify an mp4 filename"
  return 1

mp4file = new MP4File process.argv[2]
mp4file.parse()


  open: (filename) ->
    startTime = process.hrtime()
    @fileBuf = fs.readFileSync filename  # up to 1GB
    @bits = new Bits @fileBuf
    diffTime = process.hrtime startTime
    console.log "took #{(diffTime[0] * 1e9 + diffTime[1]) / 1000000} ms to read"

