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.
@@ -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