format_parser 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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