Class: FormatParser::MPEGParser

Inherits:
Object
  • Object
show all
Extended by:
IOUtils
Defined in:
lib/parsers/mpeg_parser.rb

Overview

Constant Summary collapse

ASPECT_RATIOS =
{
  1 => '1:1',
  2 => '4:3',
  3 => '16:9',
  4 => '2.21:1'
}
FRAME_RATES =
{
  1 => '23.976',
  2 => '24',
  3 => '25',
  4 => '29.97',
  5 => '30',
  6 => '50',
  7 => '59.94',
  8 => '60'
}
PACK_HEADER_START_CODE =
[0x00, 0x00, 0x01, 0xBA].pack('C*')
SEQUENCE_HEADER_START_CODE =
[0xB3].pack('C*')
MAX_BLOCK_READS =
32
BYTES_TO_READ_PER_READ =
1024

Constants included from IOUtils

IOUtils::INTEGER_DIRECTIVES

Class Method Summary collapse

Methods included from IOUtils

read_bytes, read_fixed_point, read_int, safe_read, safe_skip, skip_bytes

Class Method Details

.call(io) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/parsers/mpeg_parser.rb', line 36

def self.call(io)
  return unless matches_mpeg_header?(io)

  # We are looping though the stream because there can be several sequence headers and some of them are not useful.
  # If we detect that the header is not useful, then we look for the next one for SEEK_FOR_SEQUENCE_HEADER_TIMES_LIMIT
  # If we reach the EOF, then the mpg is likely to be corrupted and we return nil
  MAX_BLOCK_READS.times do
    next unless pos = find_next_header_code_pos(io)
    io.seek(pos + 1)
    horizontal_size, vertical_size = parse_image_size(io)
    ratio_code, rate_code = parse_rate_information(io)
    return file_info(horizontal_size, vertical_size, ratio_code, rate_code) if valid_aspect_ratio_code?(ratio_code) && valid_frame_rate_code?(rate_code)
  end
  nil # otherwise the return value of Integer#times will be returned
rescue FormatParser::IOUtils::InvalidRead
  nil
end

.convert_3_bytes_to_bits(bytes) ⇒ Object



103
104
105
106
# File 'lib/parsers/mpeg_parser.rb', line 103

def self.convert_3_bytes_to_bits(bytes)
  bytes = bytes.unpack('CCC')
  (bytes[0] << 16) | (bytes[1] << 8) | (bytes[2])
end

.file_info(width_px, height_px, ratio_code, rate_code) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/parsers/mpeg_parser.rb', line 54

def self.file_info(width_px, height_px, ratio_code, rate_code)
  FormatParser::Video.new(
    format: :mpg,
    width_px: width_px,
    height_px: height_px,
    intrinsics: {
      aspect_ratio: ASPECT_RATIOS.fetch(ratio_code),
      frame_rate: FRAME_RATES.fetch(rate_code)
    },
  )
end

.find_next_header_code_pos(io) ⇒ Object

Returns the position of the next sequence package content in the stream This method will read BYTES_TO_READ_PER_TIME in each loop for a maximum amount of SEEK_FOR_SEQUENCE_HEADER_START_CODE_TIMES_LIMIT times If the package is not found, then it returns nil.



91
92
93
94
95
96
# File 'lib/parsers/mpeg_parser.rb', line 91

def self.find_next_header_code_pos(io)
  pos_before_read = io.pos
  bin_str = io.read(BYTES_TO_READ_PER_READ) # bin_str might be nil if we are at EOF
  header_relative_index = bin_str && bin_str.index(SEQUENCE_HEADER_START_CODE)
  return pos_before_read + header_relative_index if header_relative_index
end

.likely_match?(filename) ⇒ Boolean

Returns:

  • (Boolean)


32
33
34
# File 'lib/parsers/mpeg_parser.rb', line 32

def self.likely_match?(filename)
  filename =~ /\.(mpg|mpeg)$/i
end

.matches_mpeg_header?(io) ⇒ Boolean

If the first 4 bytes of the stream are equal to 00 00 01 BA, the pack start code for the Pack Header, then it’s an MPEG file.

Returns:

  • (Boolean)


99
100
101
# File 'lib/parsers/mpeg_parser.rb', line 99

def self.matches_mpeg_header?(io)
  safe_read(io, 4) == PACK_HEADER_START_CODE
end

.parse_image_size(io) ⇒ Object

The following 3 bytes after the sequence header code, gives us information about the px size 1.5 bytes (12 bits) for horizontal size and 1.5 bytes for vertical size



68
69
70
71
# File 'lib/parsers/mpeg_parser.rb', line 68

def self.parse_image_size(io)
  image_size = convert_3_bytes_to_bits(safe_read(io, 3))
  [read_first_12_bits(image_size), read_last_12_bits(image_size)]
end

.parse_rate_information(io) ⇒ Object

The following byte gives us information about the aspect ratio and frame rate 4 bits corresponds to the aspect ratio and 4 bits to the frame rate code



75
76
77
78
# File 'lib/parsers/mpeg_parser.rb', line 75

def self.parse_rate_information(io)
  rate_information = safe_read(io, 1).unpack('C').first
  [read_first_4_bits(rate_information), read_last_4_bits(rate_information)]
end

.read_first_12_bits(bits) ⇒ Object



108
109
110
# File 'lib/parsers/mpeg_parser.rb', line 108

def self.read_first_12_bits(bits)
  bits >> 12 & 0x0fff
end

.read_first_4_bits(byte) ⇒ Object



116
117
118
# File 'lib/parsers/mpeg_parser.rb', line 116

def self.read_first_4_bits(byte)
  byte >> 4
end

.read_last_12_bits(bits) ⇒ Object



112
113
114
# File 'lib/parsers/mpeg_parser.rb', line 112

def self.read_last_12_bits(bits)
  bits & 0x0fff
end

.read_last_4_bits(byte) ⇒ Object



120
121
122
# File 'lib/parsers/mpeg_parser.rb', line 120

def self.read_last_4_bits(byte)
  byte & 0x0F
end

.valid_aspect_ratio_code?(ratio_code) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/parsers/mpeg_parser.rb', line 80

def self.valid_aspect_ratio_code?(ratio_code)
  ASPECT_RATIOS.include?(ratio_code)
end

.valid_frame_rate_code?(rate_code) ⇒ Boolean

Returns:

  • (Boolean)


84
85
86
# File 'lib/parsers/mpeg_parser.rb', line 84

def self.valid_frame_rate_code?(rate_code)
  FRAME_RATES.include?(rate_code)
end