format_parser 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -1
- data/README.md +80 -68
- data/lib/format_parser/version.rb +1 -1
- data/lib/io_utils.rb +30 -6
- data/lib/parsers/cr3_parser/decoder.rb +35 -0
- data/lib/parsers/cr3_parser.rb +51 -0
- data/lib/parsers/heif_parser.rb +7 -7
- data/lib/parsers/iso_base_media_file_format/decoder.rb +1041 -0
- data/lib/parsers/jpeg_parser.rb +1 -1
- data/lib/parsers/moov_parser/decoder.rb +1 -1
- data/lib/parsers/moov_parser.rb +9 -9
- data/lib/parsers/webp_parser.rb +6 -6
- data/spec/format_parser_spec.rb +3 -1
- data/spec/parsers/cr3_parser_spec.rb +58 -0
- data/spec/parsers/iso_base_media_file_format/decoder_spec.rb +242 -0
- metadata +11 -6
@@ -0,0 +1,1041 @@
|
|
1
|
+
# This class provides generic methods for parsing file formats based on QuickTime-style "atoms", such as those seen in
|
2
|
+
# the ISO base media file format (ISO/IEC 14496-12), a.k.a MPEG-4, and those that extend it (MP4, CR3, HEIF, etc.).
|
3
|
+
#
|
4
|
+
# For more information on atoms, see https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap1/qtff1.html
|
5
|
+
# or https://b.goeswhere.com/ISO_IEC_14496-12_2015.pdf.
|
6
|
+
#
|
7
|
+
# TODO: The vast majority of the methods have been commented out here. This decision was taken to expedite the release
|
8
|
+
# of support for the CR3 format, such that it was not blocked by the undertaking of testing this class in its
|
9
|
+
# entirety. We should migrate existing formats that are based on the ISO base media file format and reintroduce these
|
10
|
+
# methods with tests down-the-line.
|
11
|
+
|
12
|
+
module FormatParser
|
13
|
+
module ISOBaseMediaFileFormat
|
14
|
+
class Decoder
|
15
|
+
include FormatParser::IOUtils
|
16
|
+
|
17
|
+
class Atom < Struct.new(:type, :position, :size, :fields, :children)
|
18
|
+
def initialize(type, position, size, fields = nil, children = nil)
|
19
|
+
super
|
20
|
+
self.fields ||= {}
|
21
|
+
self.children ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Find and return the first descendent (using depth-first search) of a given type.
|
25
|
+
#
|
26
|
+
# @param [Array<String>] types
|
27
|
+
# @return [Atom, nil]
|
28
|
+
def find_first_descendent(types)
|
29
|
+
children.each do |child|
|
30
|
+
return child if types.include?(child.type)
|
31
|
+
if (descendent = child.find_first_descendent(types))
|
32
|
+
return descendent
|
33
|
+
end
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Find and return all descendents of a given type.
|
39
|
+
#
|
40
|
+
# @param [Array<String>] types
|
41
|
+
# @return [Array<Atom>]
|
42
|
+
def select_descendents(types)
|
43
|
+
children.map do |child|
|
44
|
+
descendents = child.select_descendents(types)
|
45
|
+
types.include?(child.type) ? [child] + descendents : descendents
|
46
|
+
end.flatten
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param [Integer] max_read
|
51
|
+
# @param [IO, FormatParser::IOConstraint] io
|
52
|
+
# @return [Array<Atom>]
|
53
|
+
def build_atom_tree(max_read, io = nil)
|
54
|
+
@buf = FormatParser::IOConstraint.new(io) if io
|
55
|
+
raise ArgumentError, "IO missing - supply a valid IO object" unless @buf
|
56
|
+
atoms = []
|
57
|
+
max_pos = @buf.pos + max_read
|
58
|
+
loop do
|
59
|
+
break if @buf.pos >= max_pos
|
60
|
+
atom = parse_atom
|
61
|
+
break unless atom
|
62
|
+
atoms << atom
|
63
|
+
end
|
64
|
+
atoms
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
# A mapping of atom types to their respective parser methods. Each method must take a single Integer parameter, size,
|
70
|
+
# and return the atom's fields and children where appropriate as a Hash and Array of Atoms respectively.
|
71
|
+
ATOM_PARSERS = {
|
72
|
+
# 'bxml' => :bxml,
|
73
|
+
# 'co64' => :co64,
|
74
|
+
# 'cprt' => :cprt,
|
75
|
+
# 'cslg' => :cslg,
|
76
|
+
# 'ctts' => :ctts,
|
77
|
+
'dinf' => :container,
|
78
|
+
# 'dref' => :dref,
|
79
|
+
'edts' => :container,
|
80
|
+
# 'fecr' => :fecr,
|
81
|
+
# 'fiin' => :fiin,
|
82
|
+
# 'fire' => :fire,
|
83
|
+
# 'fpar' => :fpar,
|
84
|
+
# 'ftyp' => :typ,
|
85
|
+
# 'gitn' => :gitn,
|
86
|
+
# 'hdlr' => :hdlr,
|
87
|
+
# 'hmhd' => :hmhd,
|
88
|
+
# 'iinf' => :iinf,
|
89
|
+
# 'iloc' => :iloc,
|
90
|
+
# 'infe' => :infe,
|
91
|
+
# 'ipro' => :ipro,
|
92
|
+
# 'iref' => :iref,
|
93
|
+
# 'leva' => :leva,
|
94
|
+
# 'mdhd' => :mdhd,
|
95
|
+
'mdia' => :container,
|
96
|
+
'meco' => :container,
|
97
|
+
# 'mehd' => :mehd,
|
98
|
+
# 'mere' => :mere,
|
99
|
+
# 'meta' => :meta,
|
100
|
+
# 'mfhd' => :mfhd,
|
101
|
+
'mfra' => :container,
|
102
|
+
# 'mfro' => :mfro,
|
103
|
+
'minf' => :container,
|
104
|
+
'moof' => :container,
|
105
|
+
'moov' => :container,
|
106
|
+
'mvex' => :container,
|
107
|
+
# 'mvhd' => :mvhd,
|
108
|
+
'nmhd' => :empty,
|
109
|
+
# 'padb' => :padb,
|
110
|
+
'paen' => :container,
|
111
|
+
# 'pdin' => :pdin,
|
112
|
+
# 'pitm' => :pitm,
|
113
|
+
# 'prft' => :prft,
|
114
|
+
# 'saio' => :saio,
|
115
|
+
# 'saiz' => :saiz,
|
116
|
+
# 'sbgp' => :sbgp,
|
117
|
+
'schi' => :container,
|
118
|
+
# 'schm' => :schm,
|
119
|
+
# 'sdtp' => :sdtp,
|
120
|
+
# 'segr' => :segr,
|
121
|
+
# 'sgpd' => :sgpd,
|
122
|
+
# 'sidx' => :sidx,
|
123
|
+
'sinf' => :container,
|
124
|
+
# 'smhd' => :smhd,
|
125
|
+
# 'ssix' => :ssix,
|
126
|
+
'stbl' => :container,
|
127
|
+
# 'stco' => :stco,
|
128
|
+
# 'stdp' => :stdp,
|
129
|
+
'sthd' => :empty,
|
130
|
+
'strd' => :container,
|
131
|
+
# 'stri' => :stri,
|
132
|
+
'strk' => :container,
|
133
|
+
# 'stsc' => :stsc,
|
134
|
+
# 'stsd' => :stsd,
|
135
|
+
# 'stsh' => :stsh,
|
136
|
+
# 'stss' => :stss,
|
137
|
+
# 'stsz' => :stsz,
|
138
|
+
# 'stts' => :stts,
|
139
|
+
# 'styp' => :typ,
|
140
|
+
# 'stz2' => :stz2,
|
141
|
+
# 'subs' => :subs,
|
142
|
+
# 'tfra' => :tfra,
|
143
|
+
# 'tkhd' => :tkhd,
|
144
|
+
'trak' => :container,
|
145
|
+
# 'trex' => :trex,
|
146
|
+
# 'tsel' => :tsel,
|
147
|
+
'udta' => :container,
|
148
|
+
# 'url ' => :dref_url,
|
149
|
+
# 'urn ' => :dref_urn,
|
150
|
+
'uuid' => :uuid,
|
151
|
+
# 'vmhd' => :vmhd,
|
152
|
+
# 'xml ' => :xml,
|
153
|
+
}
|
154
|
+
|
155
|
+
# Parse the atom at the IO's current position.
|
156
|
+
#
|
157
|
+
# @return [Atom, nil]
|
158
|
+
def parse_atom
|
159
|
+
position = @buf.pos
|
160
|
+
|
161
|
+
size = read_int_32
|
162
|
+
type = read_string(4)
|
163
|
+
size = read_int_64 if size == 1
|
164
|
+
body_size = size - (@buf.pos - position)
|
165
|
+
next_atom_position = position + size
|
166
|
+
|
167
|
+
if self.class::ATOM_PARSERS.include?(type)
|
168
|
+
fields, children = method(self.class::ATOM_PARSERS[type]).call(body_size)
|
169
|
+
if @buf.pos != next_atom_position
|
170
|
+
# We should never end up in this state. If we do, it likely indicates a bug in the atom's parser method.
|
171
|
+
warn("Unexpected IO position after parsing #{type} atom at position #{position}. Atom size: #{size}. Expected position: #{next_atom_position}. Actual position: #{@buf.pos}.")
|
172
|
+
@buf.seek(next_atom_position)
|
173
|
+
end
|
174
|
+
Atom.new(type, position, size, fields, children)
|
175
|
+
else
|
176
|
+
skip_bytes(body_size)
|
177
|
+
Atom.new(type, position, size)
|
178
|
+
end
|
179
|
+
rescue FormatParser::IOUtils::InvalidRead
|
180
|
+
nil
|
181
|
+
end
|
182
|
+
|
183
|
+
# Parse any atom that serves as a container, with only children and no fields of its own.
|
184
|
+
def container(size)
|
185
|
+
[nil, build_atom_tree(size)]
|
186
|
+
end
|
187
|
+
|
188
|
+
# Parse only an atom's version and flags, skipping the remainder of the atom's body.
|
189
|
+
def empty(size)
|
190
|
+
fields = read_version_and_flags
|
191
|
+
skip_bytes(size - 4)
|
192
|
+
[fields, nil]
|
193
|
+
end
|
194
|
+
|
195
|
+
# Parse a binary XML atom.
|
196
|
+
# def bxml(size)
|
197
|
+
# fields = read_version_and_flags.merge({
|
198
|
+
# data: (size - 4).times.map { read_int_8 }
|
199
|
+
# })
|
200
|
+
# [fields, nil]
|
201
|
+
# end
|
202
|
+
|
203
|
+
# Parse a chunk large offset atom.
|
204
|
+
# def co64(_)
|
205
|
+
# fields = read_version_and_flags
|
206
|
+
# entry_count = read_int_32
|
207
|
+
# fields.merge!({
|
208
|
+
# entry_count: entry_count,
|
209
|
+
# entries: entry_count.times.map { { chunk_offset: read_int_64 } }
|
210
|
+
# })
|
211
|
+
# [fields, nil]
|
212
|
+
# end
|
213
|
+
|
214
|
+
# Parse a copyright atom.
|
215
|
+
# def cprt(size)
|
216
|
+
# fields = read_version_and_flags
|
217
|
+
# tmp = read_int_16
|
218
|
+
# fields.merge!({
|
219
|
+
# language: [(tmp >> 10) & 0x1F, (tmp >> 5) & 0x1F, tmp & 0x1F],
|
220
|
+
# notice: read_string(size - 6)
|
221
|
+
# })
|
222
|
+
# [fields, nil]
|
223
|
+
# end
|
224
|
+
|
225
|
+
# Parse a composition to decode atom.
|
226
|
+
# def cslg(_)
|
227
|
+
# fields = read_version_and_flags
|
228
|
+
# version = fields[:version]
|
229
|
+
# fields.merge!({
|
230
|
+
# composition_to_dts_shift: version == 1 ? read_int_64 : read_int_32,
|
231
|
+
# least_decode_to_display_delta: version == 1 ? read_int_64 : read_int_32,
|
232
|
+
# greatest_decode_to_display_delta: version == 1 ? read_int_64 : read_int_32,
|
233
|
+
# composition_start_time: version == 1 ? read_int_64 : read_int_32,
|
234
|
+
# composition_end_time: version == 1 ? read_int_64 : read_int_32,
|
235
|
+
# })
|
236
|
+
# [fields, nil]
|
237
|
+
# end
|
238
|
+
|
239
|
+
# Parse a composition time to sample atom.
|
240
|
+
# def ctts(_)
|
241
|
+
# fields = read_version_and_flags
|
242
|
+
# entry_count = read_int_32
|
243
|
+
# fields.merge!({
|
244
|
+
# entry_count: entry_count,
|
245
|
+
# entries: entry_count.times.map do
|
246
|
+
# {
|
247
|
+
# sample_count: read_int_32,
|
248
|
+
# sample_offset: read_int_32
|
249
|
+
# }
|
250
|
+
# end
|
251
|
+
# })
|
252
|
+
# [fields, nil]
|
253
|
+
# end
|
254
|
+
|
255
|
+
# Parse a data reference atom.
|
256
|
+
# def dref(size)
|
257
|
+
# fields = read_version_and_flags.merge({
|
258
|
+
# entry_count: read_int_32
|
259
|
+
# })
|
260
|
+
# [fields, build_atom_tree(size - 8)]
|
261
|
+
# end
|
262
|
+
|
263
|
+
# Parse a data reference URL entry atom.
|
264
|
+
# def dref_url(size)
|
265
|
+
# fields = read_version_and_flags.merge({
|
266
|
+
# location: read_string(size - 4)
|
267
|
+
# })
|
268
|
+
# [fields, nil]
|
269
|
+
# end
|
270
|
+
|
271
|
+
# Parse a data reference URN entry atom.
|
272
|
+
# def dref_urn(size)
|
273
|
+
# fields = read_version_and_flags
|
274
|
+
# name, location = read_bytes(size - 4).unpack('Z2')
|
275
|
+
# fields.merge!({
|
276
|
+
# name: name,
|
277
|
+
# location: location
|
278
|
+
# })
|
279
|
+
# [fields, nil]
|
280
|
+
# end
|
281
|
+
|
282
|
+
# Parse an FEC reservoir atom.
|
283
|
+
# def fecr(_)
|
284
|
+
# fields = read_version_and_flags
|
285
|
+
# version = fields[:version]
|
286
|
+
# entry_count = version == 0 ? read_int_16 : read_int_32
|
287
|
+
# fields.merge!({
|
288
|
+
# entry_count: entry_count,
|
289
|
+
# entries: entry_count.times.map do
|
290
|
+
# {
|
291
|
+
# item_id: version == 0 ? read_int_16 : read_int_32,
|
292
|
+
# symbol_count: read_int_8
|
293
|
+
# }
|
294
|
+
# end
|
295
|
+
# })
|
296
|
+
# end
|
297
|
+
|
298
|
+
# Parse an FD item information atom.
|
299
|
+
# def fiin(size)
|
300
|
+
# fields = read_version_and_flags.merge({
|
301
|
+
# entry_count: read_int_16
|
302
|
+
# })
|
303
|
+
# [fields, build_atom_tree(size - 6)]
|
304
|
+
# end
|
305
|
+
|
306
|
+
# Parse a file reservoir atom.
|
307
|
+
# def fire(_)
|
308
|
+
# fields = read_version_and_flags
|
309
|
+
# entry_count = version == 0 ? read_int_16 : read_int_32
|
310
|
+
# fields.merge!({
|
311
|
+
# entry_count: entry_count,
|
312
|
+
# entries: entry_count.times.map do
|
313
|
+
# {
|
314
|
+
# item_id: version == 0 ? read_int_16 : read_int_32,
|
315
|
+
# symbol_count: read_int_32
|
316
|
+
# }
|
317
|
+
# end
|
318
|
+
# })
|
319
|
+
# [fields, nil]
|
320
|
+
# end
|
321
|
+
|
322
|
+
# Parse a file partition atom.
|
323
|
+
# def fpar(_)
|
324
|
+
# fields = read_version_and_flags
|
325
|
+
# version = fields[:version]
|
326
|
+
# fields.merge!({
|
327
|
+
# item_id: version == 0 ? read_int_16 : read_int_32,
|
328
|
+
# packet_payload_size: read_int_16,
|
329
|
+
# fec_encoding_id: skip_bytes(1) { read_int_8 },
|
330
|
+
# fec_instance_id: read_int_16,
|
331
|
+
# max_source_block_length: read_int_16,
|
332
|
+
# encoding_symbol_length: read_int_16,
|
333
|
+
# max_number_of_encoding_symbols: read_int_16,
|
334
|
+
# })
|
335
|
+
# # TODO: Parse scheme_specific_info, entry_count and entries { block_count, block_size }.
|
336
|
+
# skip_bytes(size - 20)
|
337
|
+
# skip_bytes(2) if version == 0
|
338
|
+
# [fields, nil]
|
339
|
+
# end
|
340
|
+
|
341
|
+
# Parse a group ID to name atom.
|
342
|
+
# def gitn(size)
|
343
|
+
# fields = read_version_and_flags
|
344
|
+
# entry_count = read_int_16
|
345
|
+
# fields.merge!({
|
346
|
+
# entry_count: entry_count
|
347
|
+
# })
|
348
|
+
# # TODO: Parse entries.
|
349
|
+
# skip_bytes(size - 6)
|
350
|
+
# [fields, nil]
|
351
|
+
# end
|
352
|
+
|
353
|
+
# Parse a handler atom.
|
354
|
+
# def hdlr(size)
|
355
|
+
# fields = read_version_and_flags.merge({
|
356
|
+
# handler_type: skip_bytes(4) { read_int_32 },
|
357
|
+
# name: skip_bytes(12) { read_string(size - 24) }
|
358
|
+
# })
|
359
|
+
# [fields, nil]
|
360
|
+
# end
|
361
|
+
|
362
|
+
# Parse a hint media header atom.
|
363
|
+
# def hmhd(_)
|
364
|
+
# fields = read_version_and_flags.merge({
|
365
|
+
# max_pdu_size: read_int_16,
|
366
|
+
# avg_pdu_size: read_int_16,
|
367
|
+
# max_bitrate: read_int_32,
|
368
|
+
# avg_bitrate: read_int_32
|
369
|
+
# })
|
370
|
+
# skip_bytes(4)
|
371
|
+
# [fields, nil]
|
372
|
+
# end
|
373
|
+
|
374
|
+
# Parse an item info atom.
|
375
|
+
# def iinf(size)
|
376
|
+
# fields = read_version_and_flags.merge({
|
377
|
+
# entry_count: version == 0 ? read_int_16 : read_int_32
|
378
|
+
# })
|
379
|
+
# [fields, build_atom_tree(size - 8)]
|
380
|
+
# end
|
381
|
+
|
382
|
+
# Parse an item location atom.
|
383
|
+
# def iloc(_)
|
384
|
+
# fields = read_version_and_flags
|
385
|
+
# tmp = read_int_16
|
386
|
+
# item_count = if version < 2
|
387
|
+
# read_int_16
|
388
|
+
# elsif version == 2
|
389
|
+
# read_int_32
|
390
|
+
# end
|
391
|
+
# offset_size = (tmp >> 12) & 0x7
|
392
|
+
# length_size = (tmp >> 8) & 0x7
|
393
|
+
# base_offset_size = (tmp >> 4) & 0x7
|
394
|
+
# index_size = tmp & 0x7
|
395
|
+
# fields.merge!({
|
396
|
+
# offset_size: offset_size,
|
397
|
+
# length_size: length_size,
|
398
|
+
# base_offset_size: base_offset_size,
|
399
|
+
# item_count: item_count,
|
400
|
+
# items: item_count.times.map do
|
401
|
+
# item = {
|
402
|
+
# item_id: if version < 2
|
403
|
+
# read_int_16
|
404
|
+
# elsif version == 2
|
405
|
+
# read_int_32
|
406
|
+
# end
|
407
|
+
# }
|
408
|
+
# item[:construction_method] = read_int_16 & 0x7 if version == 1 || version == 2
|
409
|
+
# item[:data_reference_index] = read_int_16
|
410
|
+
# skip_bytes(base_offset_size) # TODO: Dynamically parse base_offset based on base_offset_size
|
411
|
+
# extent_count = read_int_16
|
412
|
+
# item[:extent_count] = extent_count
|
413
|
+
# # TODO: Dynamically parse extent_index, extent_offset and extent_length based on their respective sizes.
|
414
|
+
# skip_bytes(extent_count * (offset_size + length_size))
|
415
|
+
# skip_bytes(extent_count * index_size) if (version == 1 || version == 2) && index_size > 0
|
416
|
+
# end
|
417
|
+
# })
|
418
|
+
# end
|
419
|
+
|
420
|
+
# Parse an item info entry atom.
|
421
|
+
# def infe(size)
|
422
|
+
# # TODO: This atom is super-complicated with optional and/or version-dependent fields and children.
|
423
|
+
# empty(size)
|
424
|
+
# end
|
425
|
+
|
426
|
+
# Parse an item protection atom.
|
427
|
+
# def ipro(size)
|
428
|
+
# fields = read_version_and_flags.merge({
|
429
|
+
# protection_count: read_int_16
|
430
|
+
# })
|
431
|
+
# [fields, build_atom_tree(size - 6)]
|
432
|
+
# end
|
433
|
+
|
434
|
+
# Parse an item reference atom.
|
435
|
+
# def iref(_)
|
436
|
+
# [read_version_and_flags, build_atom_tree(size - 4)]
|
437
|
+
# end
|
438
|
+
|
439
|
+
# Parse a level assignment atom.
|
440
|
+
# def leva(_)
|
441
|
+
# fields = read_version_and_flags
|
442
|
+
# level_count = read_int_8
|
443
|
+
# fields.merge!({
|
444
|
+
# level_count: level_count,
|
445
|
+
# levels: level_count.times.map do
|
446
|
+
# track_id = read_int_32
|
447
|
+
# tmp = read_int_8
|
448
|
+
# assignment_type = tmp & 0x7F
|
449
|
+
# level = {
|
450
|
+
# track_id: track_id,
|
451
|
+
# padding_flag: tmp >> 7,
|
452
|
+
# assignment_type: assignment_type
|
453
|
+
# }
|
454
|
+
# if assignment_type == 0
|
455
|
+
# level[:grouping_type] = read_int_32
|
456
|
+
# elsif assignment_type == 1
|
457
|
+
# level.merge!({
|
458
|
+
# grouping_type: read_int_32,
|
459
|
+
# grouping_type_parameter: read_int_32
|
460
|
+
# })
|
461
|
+
# elsif assignment_type == 4
|
462
|
+
# level[:sub_track_id] = read_int_32
|
463
|
+
# end
|
464
|
+
# level
|
465
|
+
# end
|
466
|
+
# })
|
467
|
+
# [fields, nil]
|
468
|
+
# end
|
469
|
+
|
470
|
+
# Parse a media header atom.
|
471
|
+
# def mdhd(_)
|
472
|
+
# fields = read_version_and_flags
|
473
|
+
# version = fields[:version]
|
474
|
+
# fields.merge!({
|
475
|
+
# creation_time: version == 1 ? read_int_64 : read_int_32,
|
476
|
+
# modification_time: version == 1 ? read_int_64 : read_int_32,
|
477
|
+
# timescale: read_int_32,
|
478
|
+
# duration: version == 1 ? read_int_64 : read_int_32,
|
479
|
+
# })
|
480
|
+
# tmp = read_int_16
|
481
|
+
# fields[:language] = [(tmp >> 10) & 0x1F, (tmp >> 5) & 0x1F, tmp & 0x1F]
|
482
|
+
# skip_bytes(2)
|
483
|
+
# [fields, nil]
|
484
|
+
# end
|
485
|
+
|
486
|
+
# Parse a movie extends header atom.
|
487
|
+
# def mehd(_)
|
488
|
+
# fields = read_version_and_flags
|
489
|
+
# version = fields[:version]
|
490
|
+
# fields[:fragment_duration] = version == 1 ? read_int_64 : read_int_32
|
491
|
+
# [fields, nil]
|
492
|
+
# end
|
493
|
+
|
494
|
+
# Parse an metabox relation atom.
|
495
|
+
# def mere(_)
|
496
|
+
# fields = read_version_and_flags.merge({
|
497
|
+
# first_metabox_handler_type: read_int_32,
|
498
|
+
# second_metabox_handler_type: read_int_32,
|
499
|
+
# metabox_relation: read_int_8
|
500
|
+
# })
|
501
|
+
# [fields, nil]
|
502
|
+
# end
|
503
|
+
|
504
|
+
# Parse a meta atom.
|
505
|
+
# def meta(size)
|
506
|
+
# fields = read_version_and_flags
|
507
|
+
# [fields, build_atom_tree(size - 4)]
|
508
|
+
# end
|
509
|
+
|
510
|
+
# Parse a movie fragment header atom.
|
511
|
+
# def mfhd(_)
|
512
|
+
# fields = read_version_and_flags.merge({
|
513
|
+
# sequence_number: read_int_32
|
514
|
+
# })
|
515
|
+
# [fields, nil]
|
516
|
+
# end
|
517
|
+
|
518
|
+
# Parse a movie fragment random access offset atom.
|
519
|
+
# def mfro(_)
|
520
|
+
# fields = read_version_and_flags.merge({
|
521
|
+
# size: read_int_32
|
522
|
+
# })
|
523
|
+
# [fields, nil]
|
524
|
+
# end
|
525
|
+
|
526
|
+
# Parse a movie header atom.
|
527
|
+
# def mvhd(_)
|
528
|
+
# fields = read_version_and_flags
|
529
|
+
# version = fields[:version]
|
530
|
+
# fields.merge!({
|
531
|
+
# creation_time: version == 1 ? read_int_64 : read_int_32,
|
532
|
+
# modification_time: version == 1 ? read_int_64 : read_int_32,
|
533
|
+
# timescale: read_int_32,
|
534
|
+
# duration: version == 1 ? read_int_64 : read_int_32,
|
535
|
+
# rate: read_fixed_point_32,
|
536
|
+
# volume: read_fixed_point_16,
|
537
|
+
# matrix: skip_bytes(10) { read_matrix },
|
538
|
+
# next_trak_id: skip_bytes(24) { read_int_32 },
|
539
|
+
# })
|
540
|
+
# [fields, nil]
|
541
|
+
# end
|
542
|
+
|
543
|
+
# Parse a padding bits atom.
|
544
|
+
# def padb(_)
|
545
|
+
# fields = read_version_and_flags
|
546
|
+
# sample_count = read_int_32
|
547
|
+
# fields.merge!({
|
548
|
+
# sample_count: sample_count,
|
549
|
+
# padding: ((sample_count + 1) / 2).times.map do
|
550
|
+
# tmp = read_int_8
|
551
|
+
# {
|
552
|
+
# padding_1: tmp >> 4,
|
553
|
+
# padding_2: tmp & 0x07
|
554
|
+
# }
|
555
|
+
# end
|
556
|
+
# })
|
557
|
+
# [fields, nil]
|
558
|
+
# end
|
559
|
+
|
560
|
+
# Parse a progressive download information atom.
|
561
|
+
# def pdin(size)
|
562
|
+
# fields = read_version_and_flags.merge({
|
563
|
+
# entries: ((size - 4) / 8).times.map do
|
564
|
+
# {
|
565
|
+
# rate: read_int_32,
|
566
|
+
# initial_delay: read_int_32
|
567
|
+
# }
|
568
|
+
# end
|
569
|
+
# })
|
570
|
+
# [fields, nil]
|
571
|
+
# end
|
572
|
+
|
573
|
+
# Parse a primary item atom.
|
574
|
+
# def pitm(_)
|
575
|
+
# fields = read_version_and_flags.merge({
|
576
|
+
# item_id: version == 0 ? read_int_16 : read_int_32
|
577
|
+
# })
|
578
|
+
# [fields, nil]
|
579
|
+
# end
|
580
|
+
|
581
|
+
# Parse a producer reference time atom.
|
582
|
+
# def prft(_)
|
583
|
+
# fields = read_version_and_flags
|
584
|
+
# version = fields[:version]
|
585
|
+
# fields.merge!({
|
586
|
+
# reference_track_id: read_int_32,
|
587
|
+
# ntp_timestamp: read_int_64,
|
588
|
+
# media_time: version == 0 ? read_int_32 : read_int_64
|
589
|
+
# })
|
590
|
+
# [fields, nil]
|
591
|
+
# end
|
592
|
+
|
593
|
+
# Parse a sample auxiliary information offsets atom.
|
594
|
+
# def saio(_)
|
595
|
+
# fields = read_version_and_flags
|
596
|
+
# version = field[:version]
|
597
|
+
# flags = fields[:flags]
|
598
|
+
# fields.merge!({
|
599
|
+
# aux_info_type: read_int_32,
|
600
|
+
# aux_info_type_parameter: read_int_32
|
601
|
+
# }) if flags & 0x1
|
602
|
+
# entry_count = read_int_32
|
603
|
+
# fields.merge!({
|
604
|
+
# entry_count: entry_count,
|
605
|
+
# offsets: entry_count.times.map { version == 0 ? read_int_32 : read_int_64 }
|
606
|
+
# })
|
607
|
+
# [fields, nil]
|
608
|
+
# end
|
609
|
+
|
610
|
+
# Parse a sample auxiliary information sizes atom.
|
611
|
+
# def saiz(_)
|
612
|
+
# fields = read_version_and_flags
|
613
|
+
# flags = fields[:flags]
|
614
|
+
# fields.merge!({
|
615
|
+
# aux_info_type: read_int_32,
|
616
|
+
# aux_info_type_parameter: read_int_32
|
617
|
+
# }) if flags & 0x1
|
618
|
+
# default_sample_info_size = read_int_8
|
619
|
+
# sample_count = read_int_32
|
620
|
+
# fields.merge!({
|
621
|
+
# default_sample_info_size: default_sample_info_size,
|
622
|
+
# sample_count: sample_count
|
623
|
+
# })
|
624
|
+
# fields[:sample_info_sizes] = sample_count.times.map { read_int_8 } if default_sample_info_size == 0
|
625
|
+
# [fields, nil]
|
626
|
+
# end
|
627
|
+
|
628
|
+
# Parse a sample to group atom.
|
629
|
+
# def sbgp(_)
|
630
|
+
# fields = read_version_and_flags
|
631
|
+
# fields[:grouping_type] = read_int_32
|
632
|
+
# fields[:grouping_type_parameter] = read_int_32 if fields[:version] == 1
|
633
|
+
# entry_count = read_int_32
|
634
|
+
# fields.merge!({
|
635
|
+
# entry_count: entry_count,
|
636
|
+
# entries: entry_count.times.map do
|
637
|
+
# {
|
638
|
+
# sample_count: read_int_32,
|
639
|
+
# group_description_index: read_int_32
|
640
|
+
# }
|
641
|
+
# end
|
642
|
+
# })
|
643
|
+
# [fields, nil]
|
644
|
+
# end
|
645
|
+
|
646
|
+
# Parse a scheme type atom.
|
647
|
+
# def schm(_)
|
648
|
+
# fields = read_version_and_flags.merge({
|
649
|
+
# scheme_type: read_string(4),
|
650
|
+
# scheme_version: read_int_32,
|
651
|
+
# })
|
652
|
+
# fields[:scheme_uri] = (size - 12).times.map { read_int_8 } if flags & 0x1 != 0
|
653
|
+
# [fields, nil]
|
654
|
+
# end
|
655
|
+
|
656
|
+
# Parse an independent and disposable samples atom.
|
657
|
+
# def sdtp(size)
|
658
|
+
# # TODO: Parsing this atom needs the sample_count from the sample size atom (`stsz`).
|
659
|
+
# empty(size)
|
660
|
+
# end
|
661
|
+
|
662
|
+
# Parse an FD session group atom.
|
663
|
+
# def segr(_)
|
664
|
+
# num_session_groups = read_int_16
|
665
|
+
# fields = {
|
666
|
+
# num_session_groups: num_session_groups,
|
667
|
+
# session_groups: num_session_groups.times.map do
|
668
|
+
# entry_count = read_int_8
|
669
|
+
# session_group = {
|
670
|
+
# entry_count: entry_count,
|
671
|
+
# entries: entry_count.times.map { { group_id: read_int_32 } }
|
672
|
+
# }
|
673
|
+
# num_channels_in_session_group = read_int_16
|
674
|
+
# session_group.merge({
|
675
|
+
# num_channels_in_session_group: num_channels_in_session_group,
|
676
|
+
# channels: num_channels_in_session_group.times.map { { hint_track_id: read_int_32 } }
|
677
|
+
# })
|
678
|
+
# end
|
679
|
+
# }
|
680
|
+
# [fields, nil]
|
681
|
+
# end
|
682
|
+
|
683
|
+
# Parse a sample group description atom.
|
684
|
+
# def sgpd(_)
|
685
|
+
# fields = read_version_and_flags
|
686
|
+
# version = fields[:version]
|
687
|
+
# fields[:grouping_type] = read_int_32
|
688
|
+
# fields[:default_length] = read_int_32 if version == 1
|
689
|
+
# fields[:default_sample_description_index] = read_int_32 if version >= 2
|
690
|
+
# entry_count = read_int_32
|
691
|
+
# fields.merge!({
|
692
|
+
# entry_count: entry_count,
|
693
|
+
# entries: entry_count.times.map do
|
694
|
+
# entry = {}
|
695
|
+
# entry[:description_length] = read_int_32 if version == 1 && fields[:default_length] == 0
|
696
|
+
# entry[:atom] = parse_atom
|
697
|
+
# end
|
698
|
+
# })
|
699
|
+
# [fields, nil]
|
700
|
+
# end
|
701
|
+
|
702
|
+
# Parse a segment index atom.
|
703
|
+
# def sidx(_)
|
704
|
+
# fields = read_version_and_flags.merge({
|
705
|
+
# reference_id: read_int_32,
|
706
|
+
# timescale: read_int_32
|
707
|
+
# })
|
708
|
+
# version = fields[:version]
|
709
|
+
# fields.merge!({
|
710
|
+
# earliest_presentation_time: version == 0 ? read_int_32 : read_int_64,
|
711
|
+
# first_offset: version == 0 ? read_int_32 : read_int_64,
|
712
|
+
# })
|
713
|
+
# reference_count = skip_bytes(2) { read_int_16 }
|
714
|
+
# fields.merge!({
|
715
|
+
# reference_count: reference_count,
|
716
|
+
# references: reference_count.times.map do
|
717
|
+
# tmp = read_int_32
|
718
|
+
# reference = {
|
719
|
+
# reference_type: tmp >> 31,
|
720
|
+
# referenced_size: tmp & 0x7FFFFFFF,
|
721
|
+
# subsegment_duration: read_int_32
|
722
|
+
# }
|
723
|
+
# tmp = read_int_32
|
724
|
+
# reference.merge({
|
725
|
+
# starts_with_sap: tmp >> 31,
|
726
|
+
# sap_type: (tmp >> 28) & 0x7,
|
727
|
+
# sap_delta_time: tmp & 0x0FFFFFFF
|
728
|
+
# })
|
729
|
+
# end
|
730
|
+
# })
|
731
|
+
# [fields, nil]
|
732
|
+
# end
|
733
|
+
|
734
|
+
# Parse a sound media header atom.
|
735
|
+
# def smhd(_)
|
736
|
+
# fields = read_version_and_flags.merge({
|
737
|
+
# balance: read_fixed_point_16,
|
738
|
+
# })
|
739
|
+
# skip_bytes(2)
|
740
|
+
# [fields, nil]
|
741
|
+
# end
|
742
|
+
|
743
|
+
# Parse a subsegment index atom.
|
744
|
+
# def ssix(_)
|
745
|
+
# fields = read_version_and_flags
|
746
|
+
# subsegment_count = read_int_32
|
747
|
+
# fields.merge!({
|
748
|
+
# subsegment_count: subsegment_count,
|
749
|
+
# subsegments: subsegment_count.times.map do
|
750
|
+
# range_count = read_int_32
|
751
|
+
# {
|
752
|
+
# range_count: range_count,
|
753
|
+
# ranges: range_count.times.map do
|
754
|
+
# tmp = read_int_32
|
755
|
+
# {
|
756
|
+
# level: tmp >> 24,
|
757
|
+
# range_size: tmp & 0x00FFFFFF
|
758
|
+
# }
|
759
|
+
# end
|
760
|
+
# }
|
761
|
+
# end
|
762
|
+
# })
|
763
|
+
# [fields, nil]
|
764
|
+
# end
|
765
|
+
|
766
|
+
# Parse a chunk offset atom.
|
767
|
+
# def stco(_)
|
768
|
+
# fields = read_version_and_flags
|
769
|
+
# entry_count = read_int_32
|
770
|
+
# fields.merge!({
|
771
|
+
# entry_count: entry_count,
|
772
|
+
# entries: entry_count.times.map { { chunk_offset: read_int_32 } }
|
773
|
+
# })
|
774
|
+
# [fields, nil]
|
775
|
+
# end
|
776
|
+
|
777
|
+
# Parse a degradation priority atom.
|
778
|
+
# def stdp(size)
|
779
|
+
# # TODO: Parsing this atom needs the sample_count from the sample size atom (`stsz`).
|
780
|
+
# empty(size)
|
781
|
+
# end
|
782
|
+
|
783
|
+
# Parse a sub track information atom.
|
784
|
+
# def stri(size)
|
785
|
+
# fields = read_version_and_flags.merge({
|
786
|
+
# switch_group: read_int_16,
|
787
|
+
# alternate_group: read_int_16,
|
788
|
+
# sub_track_id: read_int_32,
|
789
|
+
# attribute_list: ((size - 12) / 4).times.map { read_int_32 }
|
790
|
+
# })
|
791
|
+
# [fields, nil]
|
792
|
+
# end
|
793
|
+
|
794
|
+
# Parse a sample to chunk atom.
|
795
|
+
# def stsc(_)
|
796
|
+
# fields = read_version_and_flags
|
797
|
+
# entry_count = read_int_32
|
798
|
+
# fields.merge!({
|
799
|
+
# entry_count: entry_count,
|
800
|
+
# entries: entry_count.times.map do
|
801
|
+
# {
|
802
|
+
# first_chunk: read_int_32,
|
803
|
+
# samples_per_chunk: read_int_32,
|
804
|
+
# sample_description_index: read_int_32
|
805
|
+
# }
|
806
|
+
# end
|
807
|
+
# })
|
808
|
+
# [fields, nil]
|
809
|
+
# end
|
810
|
+
|
811
|
+
# Parse a sample descriptions atom.
|
812
|
+
# def stsd(size)
|
813
|
+
# fields = read_version_and_flags.merge({
|
814
|
+
# entry_count: read_int_32
|
815
|
+
# })
|
816
|
+
# [fields, build_atom_tree(size - 8)]
|
817
|
+
# end
|
818
|
+
|
819
|
+
# Parse a shadow sync sample atom.
|
820
|
+
# def stsh(_)
|
821
|
+
# fields = read_version_and_flags
|
822
|
+
# entry_count = read_int_32
|
823
|
+
# fields.merge!({
|
824
|
+
# entry_count: entry_count,
|
825
|
+
# entries: entry_count.times.map {
|
826
|
+
# {
|
827
|
+
# shadowed_sample_number: read_int_32,
|
828
|
+
# sync_sample_number: read_int_32
|
829
|
+
# }
|
830
|
+
# }
|
831
|
+
# })
|
832
|
+
# [fields, nil]
|
833
|
+
# end
|
834
|
+
|
835
|
+
# Parse a sync sample atom.
|
836
|
+
# def stss(_)
|
837
|
+
# fields = read_version_and_flags
|
838
|
+
# entry_count = read_int_32
|
839
|
+
# fields.merge!({
|
840
|
+
# entry_count: entry_count,
|
841
|
+
# entries: entry_count.times.map { { sample_number: read_int_32 } }
|
842
|
+
# })
|
843
|
+
# [fields, nil]
|
844
|
+
# end
|
845
|
+
|
846
|
+
# Parse a sample size atom.
|
847
|
+
# def stsz(_)
|
848
|
+
# fields = read_version_and_flags
|
849
|
+
# sample_size = read_int_32
|
850
|
+
# sample_count = read_int_32
|
851
|
+
# fields.merge!({
|
852
|
+
# sample_size: sample_size,
|
853
|
+
# sample_count: sample_count,
|
854
|
+
# })
|
855
|
+
# fields[:entries] = sample_count.times.map { { entry_size: read_int_32 } } if sample_size == 0
|
856
|
+
# [fields, nil]
|
857
|
+
# end
|
858
|
+
|
859
|
+
# Parse a decoding time to sample atom.
|
860
|
+
# def stts(_)
|
861
|
+
# fields = read_version_and_flags
|
862
|
+
# entry_count = read_int_32
|
863
|
+
# fields.merge!({
|
864
|
+
# entry_count: entry_count,
|
865
|
+
# entries: entry_count.times.map do
|
866
|
+
# {
|
867
|
+
# sample_count: read_int_32,
|
868
|
+
# sample_delta: read_int_32
|
869
|
+
# }
|
870
|
+
# end
|
871
|
+
# })
|
872
|
+
# [fields, nil]
|
873
|
+
# end
|
874
|
+
|
875
|
+
# Parse a compact sample size atom.
|
876
|
+
# def stz2(size)
|
877
|
+
# fields = read_version_and_flags.merge({
|
878
|
+
# field_size: skip_bytes(3) { read_int_8 },
|
879
|
+
# sample_count: read_int_32
|
880
|
+
# })
|
881
|
+
# # TODO: Handling for parsing entry sizes dynamically based on field size.
|
882
|
+
# skip_bytes(size - 12)
|
883
|
+
# [fields, nil]
|
884
|
+
# end
|
885
|
+
|
886
|
+
# Parse a sub-sample information atom.
|
887
|
+
# def subs(_)
|
888
|
+
# fields = read_version_and_flags
|
889
|
+
# entry_count = read_int_32
|
890
|
+
# fields[:entries] = entry_count.times.map do
|
891
|
+
# sample_delta = read_int_32
|
892
|
+
# subsample_count = read_int_16
|
893
|
+
# {
|
894
|
+
# sample_delta: sample_delta,
|
895
|
+
# subsample_count: subsample_count,
|
896
|
+
# subsample_information: subsample_count.times.map do
|
897
|
+
# {
|
898
|
+
# subsample_size: version == 1 ? read_int_32 : read_int_16,
|
899
|
+
# subsample_priority: read_int_8,
|
900
|
+
# discardable: read_int_8,
|
901
|
+
# codec_specific_parameters: read_int_32
|
902
|
+
# }
|
903
|
+
# end
|
904
|
+
# }
|
905
|
+
# end
|
906
|
+
# [fields, nil]
|
907
|
+
# end
|
908
|
+
|
909
|
+
# Parse a track fragment random access atom.
|
910
|
+
# def tfra(_)
|
911
|
+
# fields = read_version_and_flags
|
912
|
+
# version = fields[:version]
|
913
|
+
# fields[:track_id] = read_int_32
|
914
|
+
# skip_bytes(3)
|
915
|
+
# tmp = read_int_8
|
916
|
+
# size_of_traf_number = (tmp >> 4) & 0x3
|
917
|
+
# size_of_trun_number = (tmp >> 2) & 0x3
|
918
|
+
# size_of_sample_number = tmp & 0x3
|
919
|
+
# entry_count = read_int_32
|
920
|
+
# fields.merge!({
|
921
|
+
# size_of_traf_number: size_of_traf_number,
|
922
|
+
# size_of_trun_number: size_of_trun_number,
|
923
|
+
# size_of_sample_number: size_of_sample_number,
|
924
|
+
# entry_count: entry_count,
|
925
|
+
# entries: entry_count.times.map do
|
926
|
+
# entry = {
|
927
|
+
# time: version == 1 ? read_int_64 : read_int_32,
|
928
|
+
# moof_offset: version == 1 ? read_int_64 : read_int_32
|
929
|
+
# }
|
930
|
+
# # TODO: Handling for parsing traf_number, trun_number and sample_number dynamically based on their sizes.
|
931
|
+
# skip_bytes(size_of_traf_number + size_of_trun_number + size_of_sample_number + 3)
|
932
|
+
# entry
|
933
|
+
# end
|
934
|
+
# })
|
935
|
+
# [fields, nil]
|
936
|
+
# end
|
937
|
+
|
938
|
+
# Parse a track header atom.
|
939
|
+
# def tkhd(_)
|
940
|
+
# fields = read_version_and_flags
|
941
|
+
# version = fields[:version]
|
942
|
+
# fields.merge!({
|
943
|
+
# creation_time: version == 1 ? read_int_64 : read_int_32,
|
944
|
+
# modification_time: version == 1 ? read_int_64 : read_int_32,
|
945
|
+
# track_id: read_int_32,
|
946
|
+
# duration: skip_bytes(4) { version == 1 ? read_int_64 : read_int_32 },
|
947
|
+
# layer: skip_bytes(8) { read_int_16 },
|
948
|
+
# alternate_group: read_int_16,
|
949
|
+
# volume: read_fixed_point_16,
|
950
|
+
# matrix: skip_bytes(2) { read_matrix },
|
951
|
+
# width: read_fixed_point_32,
|
952
|
+
# height: read_fixed_point_32
|
953
|
+
# })
|
954
|
+
# [fields, nil]
|
955
|
+
# end
|
956
|
+
|
957
|
+
# Parse a track extends atom.
|
958
|
+
# def trex(_)
|
959
|
+
# fields = read_version_and_flags.merge({
|
960
|
+
# track_id: read_int_32,
|
961
|
+
# default_sample_description_index: read_int_32,
|
962
|
+
# default_sample_duration: read_int_32,
|
963
|
+
# default_sample_size: read_int_32,
|
964
|
+
# default_sample_flags: read_int_32
|
965
|
+
# })
|
966
|
+
# [fields, nil]
|
967
|
+
# end
|
968
|
+
|
969
|
+
# Parse a track selection atom.
|
970
|
+
# def tsel(size)
|
971
|
+
# fields = read_version_and_flags.merge({
|
972
|
+
# switch_group: read_int_32,
|
973
|
+
# attribute_list: ((size - 8) / 4).times.map { read_int_32 }
|
974
|
+
# })
|
975
|
+
# [fields, nil]
|
976
|
+
# end
|
977
|
+
|
978
|
+
# Parse a file/segment type compatibility atom.
|
979
|
+
# def typ(size)
|
980
|
+
# compatible_brands_count = (size - 8) / 4
|
981
|
+
# fields = {
|
982
|
+
# major_brand: read_string(4),
|
983
|
+
# minor_version: read_int_32,
|
984
|
+
# compatible_brands: compatible_brands_count.times.map { read_string(4) }
|
985
|
+
# }
|
986
|
+
# [fields, nil]
|
987
|
+
# end
|
988
|
+
|
989
|
+
# Parse a UUID atom.
|
990
|
+
def uuid(size)
|
991
|
+
fields = { usertype: read_bytes(16).unpack('H*').first }
|
992
|
+
skip_bytes(size - 16)
|
993
|
+
[fields, nil]
|
994
|
+
end
|
995
|
+
|
996
|
+
# Parse a video media header atom.
|
997
|
+
# def vmhd(_)
|
998
|
+
# fields = read_version_and_flags.merge({
|
999
|
+
# graphics_mode: read_int_16,
|
1000
|
+
# op_color: (1..3).map { read_int_16 }
|
1001
|
+
# })
|
1002
|
+
# [fields, nil]
|
1003
|
+
# end
|
1004
|
+
|
1005
|
+
# Parse an XML atom.
|
1006
|
+
# def xml(size)
|
1007
|
+
# fields = read_version_and_flags.merge({
|
1008
|
+
# xml: read_string(size - 4)
|
1009
|
+
# })
|
1010
|
+
# [fields, nil]
|
1011
|
+
# end
|
1012
|
+
|
1013
|
+
# Parse a matrix.
|
1014
|
+
#
|
1015
|
+
# Matrices are 3×3 and encoded row-by-row as 32-bit fixed-point numbers divided as 16.16, except for the rightmost
|
1016
|
+
# column which is divided as 2.30.
|
1017
|
+
#
|
1018
|
+
# See https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737.
|
1019
|
+
def read_matrix
|
1020
|
+
9.times.map do |i|
|
1021
|
+
if i % 3 == 2
|
1022
|
+
read_fixed_point_32_2_30
|
1023
|
+
else
|
1024
|
+
read_fixed_point_32
|
1025
|
+
end
|
1026
|
+
end
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
# Parse an atom's version and flags.
|
1030
|
+
#
|
1031
|
+
# It's common for atoms to begin with a single byte representing the version followed by three bytes representing any
|
1032
|
+
# associated flags. Both of these are often 0.
|
1033
|
+
def read_version_and_flags
|
1034
|
+
{
|
1035
|
+
version: read_int_8,
|
1036
|
+
flags: read_bytes(3)
|
1037
|
+
}
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
end
|