class AviGlitch::Avi

Avi parses the passed RIFF-AVI file and maintains binary data as a structured object. It contains headers, frame’s raw data, and indices of frames. The AviGlitch library accesses the data through this class internally.

Constants

MAX_RIFF_SIZE

Attributes

indices[RW]

List of indices for ‘movi’ data.

riff[RW]

Object which represents RIFF structure.

Public Class Methods

new(path = nil) click to toggle source

Generates an instance.

# File lib/aviglitch/avi.rb, line 87
def initialize path = nil
  return unless @movi.nil? # don't reconfigure the path when cloning
  self.path = path unless path.nil?
end
print_rifftree(file) click to toggle source

Parses the file and prints the RIFF structure to stdout.

rifftree(file, out = nil) click to toggle source

Parses the file and returns the RIFF structure.

# File lib/aviglitch/avi.rb, line 504
def rifftree file, out = nil
  returnable = out.nil? 
  out = StringIO.new if returnable 

  parse = ->(io, depth = 0, len = 0) do
    offset = io.pos
    while id = io.read(4) do
      if len > 0 && io.pos >= offset + len
        io.pos -= 4
        break
      end
      size = io.read(4).unpack('V').first
      str = depth > 0 ? '   ' * depth + id : id
      if id =~ /^(?:RIFF|LIST)$/
        lid = io.read(4)
        str << (' (%d)' % size) + " ’#{lid}’\n"
        out.print str
        parse.call io, depth + 1, size
      else
        str << (' (%d)' % size ) + "\n"
        out.print str
        io.pos += size
        io.pos += 1 if size % 2 == 1
      end
    end
  end

  io = file
  is_io = file.respond_to?(:seek)  # Probably IO.
  io = File.open(file, 'rb') unless is_io
  begin
    io.rewind
    parse.call io
    io.rewind
  ensure
    io.close unless is_io
  end
  
  if returnable
    out.rewind
    out.read
  end
end

Public Instance Methods

==(other) click to toggle source

Returns true if other‘s indices are same as self’s indices.

# File lib/aviglitch/avi.rb, line 390
def == other
  self.indices == other.indices
end
close() click to toggle source

Closes the file.

# File lib/aviglitch/avi.rb, line 168
def close
  @movi.close!
end
is_avi2?() click to toggle source

Detects the current data will be an AVI2.0 file.

# File lib/aviglitch/avi.rb, line 180
def is_avi2?
  @movi.size >= MAX_RIFF_SIZE
end
output(path) click to toggle source

Saves data to AVI formatted file.

# File lib/aviglitch/avi.rb, line 186
def output path
  @index_pos = 0
  # prepare headers by reusing existing ones
  strl = search 'hdrl', 'strl'
  if is_avi2?
    # indx
    vid_frames_size = 0
    @indexinfo = @indices.collect { |ix|
      vid_frames_size += 1 if ix[:id] =~ /d[bc]$/
      ix[:id]
    }.uniq.sort.collect { |d| 
      [d, {}]
    }.to_h  # should be like: {"00dc"=>{}, "01wb"=>{}}
    strl.each_with_index do |sl, i|
      indx = sl.child 'indx'
      if indx.nil?
        indx = RiffChunk.new('indx', 4120, "\0" * 4120)
        indx.value[0, 8] = [4, 0, 0, 0].pack('vccV')
        sl.value.push indx
      else
        indx.value[4, 4] = [0].pack('V')
        indx.value[24..-1] = "\0" * (indx.value.size - 24)
      end
      preid = indx.value[8, 4]
      info = @indexinfo.find do |key, val|
        # more strict way must exist though..
        if preid == "\0\0\0\0"
          key.start_with? "%02d" % i
        else
          key == preid
        end
      end
      indx.value[8, 4] = info.first if preid == "\0\0\0\0"
      info.last[:indx] = indx
      info.last[:fcc] = 'ix' + info.first[0, 2]
      info.last[:cur] = []
    end
    # odml
    odml = search('hdrl', 'odml').first
    if odml.nil?
      odml = RiffChunk.new(
        'LIST', 260, 'odml', [RiffChunk.new('dmlh', 248, "\0" * 248)]
      )
      @riff.first.child('hdrl').value.push odml
    end
    odml.child('dmlh').value[0, 4] = [@indices.size].pack('V')
  else
    strl.each do |sl|
      indx = sl.child 'indx'
      unless indx.nil?
        sl.value.delete indx
      end
    end
  end

  # movi
  write_movi = ->(io) do
    vid_frames_size = 0
    io.print 'LIST'
    io.print "\0\0\0\0"
    data_offset = io.pos
    io.print 'movi'
    while io.pos - data_offset <= MAX_RIFF_SIZE
      ix = @indices[@index_pos]
      @indexinfo[ix[:id]][:cur] << {
        pos: io.pos, size: ix[:size], flag: ix[:flag]
      } if is_avi2?
      io.print ix[:id]
      vid_frames_size += 1 if ix[:id] =~ /d[bc]$/
      io.print [ix[:size]].pack('V')
      @movi.pos += 8
      io.print @movi.read(ix[:size])
      if ix[:size] % 2 == 1
        io.print "\0"
        @movi.pos += 1
      end
      @index_pos += 1
      break if @index_pos > @indices.size - 1
    end
    # standard index
    if is_avi2?
      @indexinfo.each do |key, info|
        ix_offset = io.pos
        io.print info[:fcc]
        io.print [24 + 8 * info[:cur].size].pack('V')
        io.print [2, 0, 1, info[:cur].size].pack('vccV')
        io.print key
        io.print [data_offset, 0].pack('qV')
        info[:cur].each.with_index do |cur, i|
          io.print [cur[:pos] - data_offset + 8].pack('V') # 8 for LIST####
          sz = cur[:size]
          if cur[:flag] & Frame::AVIIF_KEYFRAME == 0 # is not keyframe
            sz = sz | 0b1000_0000_0000_0000_0000_0000_0000_0000
          end
          io.print [sz].pack('V')
        end
        # rewrite indx
        indx = info[:indx]
        nent = indx.value[4, 4].unpack('V').first + 1
        indx.value[4, 4] = [nent].pack('V')
        indx.value[24 + 16 * (nent - 1), 16] = [
          ix_offset, io.pos - ix_offset, info[:cur].size
        ].pack('qVV')
        io.pos = expected_position_of(indx) + 8
        io.print indx.value
        # clean up
        info[:cur] = []
        io.seek 0, IO::SEEK_END
      end
    end
    # size of movi
    size = io.pos - data_offset
    io.pos = data_offset - 4
    io.print [size].pack('V')
    io.seek 0, IO::SEEK_END
    io.print "\0" if size % 2 == 1
    vid_frames_size
  end

  File.open(path, 'w+') do |io|
    io.binmode
    @movi.rewind
    # normal AVI
    # header
    io.print 'RIFF'
    io.print "\0\0\0\0"
    io.print 'AVI '
    @riff.first.value.each do |chunk|
      break if chunk.id == 'movi'
      print_chunk io, chunk
    end
    # movi
    vid_size = write_movi.call io
    # rewrite frame count in avih header
    io.pos = 48
    io.print [vid_size].pack('V')
    io.seek 0, IO::SEEK_END
    # idx1
    io.print 'idx1'
    io.print [@index_pos * 16].pack('V')
    @indices[0..(@index_pos - 1)].each do |ix|
      io.print ix[:id] + [ix[:flag], ix[:offset] + 4, ix[:size]].pack('V3')
    end
    # rewrite riff chunk size
    avisize = io.pos - 8
    io.pos = 4
    io.print [avisize].pack('V')
    io.seek 0, IO::SEEK_END

    # AVI2.0
    while @index_pos < @indices.size
      io.print 'RIFF'
      io.print "\0\0\0\0"
      riff_offset = io.pos
      io.print 'AVIX'
      # movi
      write_movi.call io
      # rewrite total chunk size
      avisize = io.pos - riff_offset
      io.pos = riff_offset - 4
      io.print [avisize].pack('V')
      io.seek 0, IO::SEEK_END
    end
  end
end
parse_riff(io, target, len = 0, is_movi = false) click to toggle source

Parses the passed RIFF formated file recursively.

# File lib/aviglitch/avi.rb, line 112
def parse_riff io, target, len = 0, is_movi = false
  offset = io.pos
  binoffset = @movi.pos
  while id = io.read(4) do
    if len > 0 && io.pos >= offset + len
      io.pos -= 4
      break
    end
    size = io.read(4).unpack('V').first
    if id == 'RIFF' || id == 'LIST'
      lid = io.read(4)
      newarr = []
      chunk = RiffChunk.new id, size, lid, newarr
      target << chunk
      parse_riff io, newarr, size, lid == 'movi'
    else
      value = nil
      if is_movi
        if id =~ /^ix/
          v = io.read size
          # confirm the super index surely has information
          @superidx.each do |sidx|
            nent = sidx[4, 4].unpack('v').first
            cid = sidx[8, 4]
            nent.times do |i|
              ent = sidx[24 + 16 * i, 16]
              # we can check other informations thuogh
              valid = ent[0, 8].unpack('q').first == io.pos - v.size - 8
              parse_avi2_indices(v, binoffset) if valid
            end
          end
        else
          io.pos -= 8
          v = io.read(size + 8)
          @movi.print v
          @movi.print "\0" if size % 2 == 1
        end
      elsif id == 'idx1'
        v = io.read size
        parse_avi1_indices v unless was_avi2?
      else
        value = io.read size
        if id == 'indx'
          @superidx << value
          @was_avi2 = true
        end
      end
      chunk = RiffChunk.new id, size, value
      target << chunk
      io.pos += 1 if size % 2 == 1
    end
  end
end
process_movi(&block) click to toggle source

Provides internal accesses to movi binary data. It requires the yield block to return an array of pair values which consists of new indices array and new movi binary data.

# File lib/aviglitch/avi.rb, line 356
def process_movi &block
  @movi.rewind
  newindices, newmovi = block.call @indices, @movi
  unless @indices == newindices
    @indices.replace newindices
  end
  unless @movi == newmovi
    @movi.rewind
    newmovi.rewind
    while d = newmovi.read(BUFFER_SIZE) do
      @movi.print d
    end
    eof = @movi.pos
    @movi.truncate eof
  end
end
was_avi2?() click to toggle source

Detects the passed file was an AVI2.0 file.

# File lib/aviglitch/avi.rb, line 174
def was_avi2?
  @was_avi2
end