class FormatParser::MOOVParser

Constants

FTYP_MAP

Maps values of the “ftyp” atom to something we can reasonably call “file type” (something usable as a filename extension)

MP4_AU_MIME_TYPE

tools.ietf.org/html/rfc4337#section-2 There is also video/quicktime which we should be able to capture here, but there is currently no detection for MOVs versus MP4s

MP4_MIXED_MIME_TYPE

Public Instance Methods

call(io) click to toggle source
# File lib/parsers/moov_parser.rb, line 24
def call(io)
  return unless matches_moov_definition?(io)

  # Now we know we are in a MOOV, so go back and parse out the atom structure.
  # Parsing out the atoms does not read their contents - at least it doesn't
  # for the atoms we consider opaque (one of which is the "mdat" atom which
  # will be the prevalent part of the file body). We do not parse these huge
  # atoms - we skip over them and note where they are located.
  io.seek(0)

  # We have to tell the parser how far we are willing to go within the stream.
  # Knowing that we will bail out early anyway we will permit a large read. The
  # branch parse calls will know the maximum size to read from the parent atom
  # size that gets parsed just before.
  max_read_offset = 0xFFFFFFFF
  decoder = Decoder.new
  atom_tree = Measurometer.instrument('format_parser.Decoder.extract_atom_stream') do
    decoder.extract_atom_stream(io, max_read_offset)
  end

  ftyp_atom = decoder.find_first_atom_by_path(atom_tree, 'ftyp')
  file_type = ftyp_atom.field_value(:major_brand)

  # Try to find the width and height in the tkhd
  width, height = parse_dimensions(decoder, atom_tree)

  # Try to find the "topmost" duration (respecting edits)
  if mdhd = decoder.find_first_atom_by_path(atom_tree, 'moov', 'mvhd')
    timescale = mdhd.field_value(:tscale)
    duration = mdhd.field_value(:duration)
    media_duration_s = duration / timescale.to_f
  end

  # M4A only contains audio, while MP4 and friends can contain video.
  fmt = format_from_moov_type(file_type)
  if fmt == :m4a
    FormatParser::Audio.new(
      format: format_from_moov_type(file_type),
      media_duration_seconds: media_duration_s,
      content_type: MP4_AU_MIME_TYPE,
      intrinsics: atom_tree,
    )
  else
    FormatParser::Video.new(
      format: format_from_moov_type(file_type),
      width_px: width,
      height_px: height,
      media_duration_seconds: media_duration_s,
      content_type: MP4_MIXED_MIME_TYPE,
      intrinsics: atom_tree,
    )
  end
end
likely_match?(filename) click to toggle source
# File lib/parsers/moov_parser.rb, line 20
def likely_match?(filename)
  filename =~ /\.(mov|m4a|ma4|mp4|aac|m4v)$/i
end

Private Instance Methods

format_from_moov_type(file_type) click to toggle source
# File lib/parsers/moov_parser.rb, line 80
def format_from_moov_type(file_type)
  FTYP_MAP.fetch(file_type.downcase, :mov)
end
matches_moov_definition?(io) click to toggle source

An MPEG4/MOV/M4A will start with the “ftyp” atom. The atom must have a length of at least 8 (to accomodate the atom size and the atom type itself) plus the major and minor version fields. If we cannot find it we can be certain this is not our file.

# File lib/parsers/moov_parser.rb, line 112
def matches_moov_definition?(io)
  maybe_atom_size, maybe_ftyp_atom_signature = safe_read(io, 8).unpack('N1a4')
  minimum_ftyp_atom_size = 4 + 4 + 4 + 4
  maybe_atom_size >= minimum_ftyp_atom_size && maybe_ftyp_atom_signature == 'ftyp'
end
parse_dimensions(decoder, atom_tree) click to toggle source

The dimensions are located in tkhd atom, but in some files it is necessary to get it below the video track, because it can have other tracks such as audio which does not have the dimensions. More details in developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-DontLinkElementID_147

Returns [width, height] if the dimension is found Returns [nil, nil] if the dimension is not found

# File lib/parsers/moov_parser.rb, line 91
def parse_dimensions(decoder, atom_tree)
  video_trak_atom = decoder.find_video_trak_atom(atom_tree)

  tkhd = begin
    if video_trak_atom
      decoder.find_first_atom_by_path([video_trak_atom], 'trak', 'tkhd')
    else
      decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'tkhd')
    end
  end

  if tkhd
    [tkhd.field_value(:track_width).first, tkhd.field_value(:track_height).first]
  else
    [nil, nil]
  end
end