class BMFFGlitch::Base

Attributes

samples[RW]

Public Class Methods

new(path) click to toggle source
# File lib/bmffglitch/base.rb, line 5
def initialize(path)
  @io = File.open(path, 'rb') 
  @file_container = BMFF::FileContainer.parse(@io)
  @samples = get_samples(@file_container)
  @is_faststart = nil
end

Public Instance Methods

get_samples(file_container) click to toggle source
# File lib/bmffglitch/base.rb, line 35
def get_samples(file_container)
  samples = []
  file_container.select_descendants("trak").each do |trak|
    if !trak.select_descendants(BMFF::Box::VisualSampleEntry).empty?
      flag = BMFFGlitch::Sample::VISUALSAMPLE
    elsif !trak.select_descendants(BMFF::Box::AudioSampleEntry).empty?
      flag = BMFFGlitch::Sample::AUDIOSAMPLE
    elsif !trak.select_descendants(BMFF::Box::HintSampleEntry).empty?
      flag = BMFFGlitch::Sample::HINTSAMPLE
    else
      raise "Malformed trak"
    end
    
    box = trak.select_descendants("stsz")
    if (box == nil || box.empty?) 
      raise "Sample Size Boxes(stsz) is missing"
    end
    stsz = box[0]
    stsz.sample_count.times {|idx|
      # sample start with 1
      sample_number = idx + 1
      sample = BMFFGlitch::Sample.new(sample_number, flag)
      sample.size = (stsz.sample_size != 0) ? stsz.sample_size : stsz.entry_size[idx]
      samples.push(sample)
    }

    box = trak.select_descendants("stss")
    if box != nil && !box.empty?
      stss = box[0]
      stss.sample_number.each {|sample_number|
        sample = samples.find {|sample| (sample.sample_number == sample_number) && (sample.flag & flag != 0)}
        sample.flag |= BMFFGlitch::Sample::SYNCSAMPLE
      }
    end
    
    box = trak.select_descendants("ctts")
    if box != nil && !box.empty?
      ctts = box[0]
      sample_number = 1
      ctts.entry_count.times {|i|
        ctts.sample_count[i].times do
          sample = samples.find {|sample| (sample.sample_number == sample_number) && (sample.flag & flag != 0)}
          sample.sample_offset = ctts.sample_offset[i]
          sample_number += 1
        end
      }
    end
    
    box = trak.select_descendants("stts")
    if (box == nil || box.empty?) 
      raise "(Decoding)Time to Sample(stts) is missing"
    end
    stts = box[0]
    sample_number = 1
    stts.entry_count.times {|i|
      stts.sample_count[i].times do
        sample = samples.find {|sample| (sample.sample_number == sample_number) && (sample.flag & flag != 0)}
        sample.sample_delta = stts.sample_delta[i]
        sample_number += 1
      end
    }
    
    box = trak.select_descendants("stsc")
    if (box == nil || box.empty?) 
      raise "Sample Table Box(stsc) is missing"
    end
    stsc = box[0]
    
    box = trak.select_descendants("stco")
    if (box == nil || box.empty?) 
      raise "Chunk offset(stco) is missing"
    end
    stco = box[0]

    sample_number = 1
    chunk_number = 1
    stsc_idx = 0
    while (chunk_number <= stco.entry_count)
      if (stsc_idx == stsc.entry_count - 1 || chunk_number < stsc.first_chunk[stsc_idx+1])
        offset_from_chunk_offset = 0
        stsc.samples_per_chunk[stsc_idx].times do
          sample = samples.find {|sample| (sample.sample_number == sample_number) && (sample.flag & flag != 0)}
          sample.chunk_number = chunk_number
          sample.chunk_offset = stco.chunk_offset[chunk_number - 1]#chunk starts with 1
          sample.sample_description_index = stsc.sample_description_index[stsc_idx]
          sample.file_offset = sample.chunk_offset + offset_from_chunk_offset
          # prepare for next sample in the same chunk
          sample_number += 1
          offset_from_chunk_offset += sample.size
        end
        chunk_number += 1
      else
        stsc_idx += 1
      end
    end
  end

  @io.rewind
  samples.each {|sample|
    @io.seek(sample.file_offset, IO::SEEK_SET)
    sample.data = @io.read(sample.size)
  }
  return samples.sort {|a, b| a.file_offset <=> b.file_offset}
end
is_faststart?() click to toggle source
# File lib/bmffglitch/base.rb, line 12
def is_faststart?
  return @is_faststart if @is_faststart != nil

  # get mdat.offset which has the smallest offset from the top of file.
  box = @file_container.select_descendants("mdat")
  if (box == nil || box.empty?)
    raise "Media Data Box(mdat) is missing"
  else 
    first_mdat_offset = box.sort{|a,b| a.offset <=> b.offset }[0].offset
  end

  # get moov.offset
  box = @file_container.select_descendants("moov")
  if (box == nil || box.empty?)
    raise "Movie Box(moov) is missing"
  else
    moov_offset = box[0].offset
  end
  @is_faststart = (first_mdat_offset > moov_offset) ? true : false

  return @is_faststart
end
output(path) click to toggle source
# File lib/bmffglitch/base.rb, line 333
def output(path)
  update
  File.open(path, 'wb') do |f|
    f.write @file_container
  end
end
update() click to toggle source
# File lib/bmffglitch/base.rb, line 286
def update
  moov_offset_change = 0
  # if the file is faststart, moov box appears earlier than mdat, so we have to re-calculate the size of moov box to adjust
  # the offset of mdat.
  if is_faststart?
    # get old moov
    box = @file_container.select_descendants("moov")
    if (box == nil || box.empty?)
      raise "Movie Box(moov) is missing"
    else
      old_moov_size = box[0].to_s.size
    end
    
    # update moov
    update_moov

    new_moov_size = box[0].to_s.size

    moov_offset_change = new_moov_size - old_moov_size
  end
  
  box = @file_container.select_descendants("mdat")
  if (box == nil || box.empty?)
    raise "Media Data Box(mdat) is missing"
  elsif (box.length > 2) # delete unnecessary mdat to ease the re-calculation of offsets.
    box.sort{|a,b| a.offset <=> b.offset }[1..box.length].each {|mdat| @file_container.children.delete(mdat) }
  end
  mdat = box[0]

  # data starts from mdat.offset + size(uint32) + type(uint32) + moov_offset_change(if faststart)
  mdat_data_offset = mdat.offset + 4 + 4 + moov_offset_change 
  
  # re-calculate offset
  # Because of the nature of sample-based glitch, we need to handle all samples individually.
  # I dare to disassemble chunks and each chunk contains only one sample
  @samples.each {|sample|
    sample.chunk_offset = mdat_data_offset
    sample.file_offset = sample.chunk_offset# file_offset and chunk_offset has the same value because each chunk has only one sample
    
    mdat_data_offset += sample.data.length
  }
  
  mdat.raw_data = @samples.map {|sample| sample.data }.join

  update_moov
end
update_moov() click to toggle source
# File lib/bmffglitch/base.rb, line 140
def update_moov
  @file_container.select_descendants("trak").each do |trak|
    if !trak.select_descendants(BMFF::Box::VisualSampleEntry).empty?
      samples = @samples.find_all{|sample| sample.is_visualsample? }
    elsif !trak.select_descendants(BMFF::Box::AudioSampleEntry).empty?
      samples = @samples.find_all{|sample| sample.is_audiosample? }
    elsif !trak.select_descendants(BMFF::Box::HintSampleEntry).empty?
      samples = @samples.find_all{|sample| sample.is_hintsample? }
    else
      raise "Malformed trak"
    end

    # re-assign sample number and chunk number
    # sample_number starts from 1
    samples.each_with_index{|sample, i|
      sample.sample_number = i + 1
      sample.chunk_number = i + 1
    }
    
    box = trak.select_descendants("stsz")
    if (box == nil || box.empty?) 
      raise "Sample Size Boxes(stsz) is missing"
    end
    stsz = box[0]
    
    stsz.sample_count = samples.length
    size = samples.uniq{|sample| sample.size}
    if (size.length == 1) && (size[0].size != 0)
      # The size of all sample is the same, so we store the actual size in stsz.sample_size
      stsz.sample_size = size[0].size
      stsz.entry_size = []
    else
      # The size of sample varies(or all sample size is 0), we store each size in stsz.entry_size
      stsz.sample_size = 0
      stsz.entry_size = []
      # Do I have to sort samples?
      samples.each {|sample|
        stsz.entry_size.push(sample.size)
      }
    end

    box = trak.select_descendants("stss")
    if box != nil && !box.empty?
      stss = box[0]
      stss.sample_number = []
      samples.find_all {|sample| sample.is_syncsample?}.each {|sample|
        stss.sample_number.push(sample.sample_number)
      }
      stss.entry_count = stss.sample_number.length
    end
    
    box = trak.select_descendants("ctts")
    if box != nil && !box.empty?
      ctts = box[0]
      ctts.sample_count = []
      ctts.sample_offset = []
      sample_count = 0
      previous_sample_offset = nil
      samples.each{|sample|
        if (sample.sample_offset != previous_sample_offset)
          # store the previous values
          if (previous_sample_offset != nil)
            ctts.sample_count.push(sample_count)
            ctts.sample_offset.push(previous_sample_offset)
          end
          
          # prepare for the next value
          sample_count = 1
          previous_sample_offset = sample.sample_offset
        else
          sample_count += 1
        end
      }
      # store the last values
      ctts.sample_count.push(sample_count)
      ctts.sample_offset.push(previous_sample_offset)
      ctts.entry_count = ctts.sample_count.length
      
    end

    box = trak.select_descendants("stts")
    if box != nil && !box.empty?
      stts = box[0]
      stts.sample_count = []
      stts.sample_delta = []
      sample_count = 0
      previous_sample_delta = nil
      samples.each{|sample|
        if (sample.sample_delta != previous_sample_delta)
          # store the previous values
          if (previous_sample_delta != nil)
            stts.sample_count.push(sample_count)
            stts.sample_delta.push(previous_sample_delta)
          end
          # prepare for the next value
          sample_count = 1
          previous_sample_delta = sample.sample_delta
        else
          sample_count += 1
        end
      }
      # store the last values
      stts.sample_count.push(sample_count)
      stts.sample_delta.push(previous_sample_delta)
      stts.entry_count = stts.sample_count.length
    end
    
    box = trak.select_descendants("stco")
    if (box == nil || box.empty?) 
      raise "Chunk offset(stco) is missing"
    end
    stco = box[0]
    # Because of the nature of sample-based glitch, we need to handle all sample separately.
    # I dare to disassemble chunks and each chunk contains only one sample
    stco.entry_count = samples.length
    stco.chunk_offset = []
    samples.each {|sample|
      stco.chunk_offset.push(sample.chunk_offset)
    }
    
    box = trak.select_descendants("stsc")
    if (box == nil || box.empty?) 
      STDERR.puts "Sample Table Box(stsc) is missing"
      exit
    end
    stsc = box[0]
    # Because of the nature of sample-based glitch, we need to handle all samples individually.
    # I dare to disassemble chunks and each chunk contains only one sample

    stsc.first_chunk = []
    stsc.samples_per_chunk = []
    stsc.sample_description_index = []
    previous_sample_description_index = nil
    
    samples.each {|sample|
      if (sample.sample_description_index != previous_sample_description_index)
        stsc.first_chunk.push(sample.sample_number)
        stsc.samples_per_chunk.push(1)
        stsc.sample_description_index.push(sample.sample_description_index)
        previous_sample_description_index = sample.sample_description_index
      end
    }
    stsc.entry_count = stsc.first_chunk.length
  end
end