mp4-rb 0.1.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.
Files changed (155) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +24 -0
  3. data/Gemfile.lock +149 -0
  4. data/README.md +195 -0
  5. data/Rakefile +8 -0
  6. data/bin/build_mp4_xlsx.rb +35 -0
  7. data/bin/divide_mp4_to_segments.rb +52 -0
  8. data/lib/m4s/binary/ftyp.rb +11 -0
  9. data/lib/m4s/binary/mdat.rb +0 -0
  10. data/lib/m4s/binary/mfhd.rb +18 -0
  11. data/lib/m4s/binary/mvhd.rb +39 -0
  12. data/lib/m4s/binary/sidx.rb +38 -0
  13. data/lib/m4s/binary/styp.rb +17 -0
  14. data/lib/m4s/binary/tfdt.rb +25 -0
  15. data/lib/m4s/binary/tfhd.rb +38 -0
  16. data/lib/m4s/binary/trex.rb +25 -0
  17. data/lib/m4s/binary/trun.rb +30 -0
  18. data/lib/m4s/dash/manifest.rb +149 -0
  19. data/lib/m4s/layout/atom.rb +44 -0
  20. data/lib/m4s/layout/builder.rb +32 -0
  21. data/lib/m4s/layout/container.rb +59 -0
  22. data/lib/m4s/layout/ctts.rb +20 -0
  23. data/lib/m4s/layout/dinf.rb +17 -0
  24. data/lib/m4s/layout/elst.rb +33 -0
  25. data/lib/m4s/layout/ftyp.rb +21 -0
  26. data/lib/m4s/layout/hdlr.rb +22 -0
  27. data/lib/m4s/layout/init_mp4_builder.rb +57 -0
  28. data/lib/m4s/layout/mdat.rb +61 -0
  29. data/lib/m4s/layout/mdhd.rb +25 -0
  30. data/lib/m4s/layout/mfhd.rb +17 -0
  31. data/lib/m4s/layout/mvhd.rb +32 -0
  32. data/lib/m4s/layout/sidx.rb +36 -0
  33. data/lib/m4s/layout/stco.rb +21 -0
  34. data/lib/m4s/layout/stsc.rb +20 -0
  35. data/lib/m4s/layout/stsd.rb +17 -0
  36. data/lib/m4s/layout/stss.rb +20 -0
  37. data/lib/m4s/layout/stsz.rb +19 -0
  38. data/lib/m4s/layout/stts.rb +20 -0
  39. data/lib/m4s/layout/styp.rb +17 -0
  40. data/lib/m4s/layout/tfdt.rb +21 -0
  41. data/lib/m4s/layout/tfhd.rb +17 -0
  42. data/lib/m4s/layout/tkhd.rb +34 -0
  43. data/lib/m4s/layout/trex.rb +33 -0
  44. data/lib/m4s/layout/trun.rb +89 -0
  45. data/lib/m4s/layout/vsmhd.rb +51 -0
  46. data/lib/m4s/m3u8/general_playlist.rb +52 -0
  47. data/lib/m4s/m3u8/media_playlist.rb +32 -0
  48. data/lib/mp4/binary/avc1.rb +98 -0
  49. data/lib/mp4/binary/box_head.rb +8 -0
  50. data/lib/mp4/binary/co64.rb +11 -0
  51. data/lib/mp4/binary/codec.rb +8 -0
  52. data/lib/mp4/binary/ctts.rb +14 -0
  53. data/lib/mp4/binary/ctts_reader.rb +5 -0
  54. data/lib/mp4/binary/dinf.rb +5 -0
  55. data/lib/mp4/binary/dref.rb +5 -0
  56. data/lib/mp4/binary/elst.rb +16 -0
  57. data/lib/mp4/binary/fixed_point16.rb +19 -0
  58. data/lib/mp4/binary/fixed_point8.rb +19 -0
  59. data/lib/mp4/binary/ftyp.rb +9 -0
  60. data/lib/mp4/binary/gmhd.rb +5 -0
  61. data/lib/mp4/binary/hdlr.rb +13 -0
  62. data/lib/mp4/binary/hev1.rb +73 -0
  63. data/lib/mp4/binary/hmhd.rb +14 -0
  64. data/lib/mp4/binary/iods.rb +5 -0
  65. data/lib/mp4/binary/mdhd.rb +18 -0
  66. data/lib/mp4/binary/mp4a.rb +76 -0
  67. data/lib/mp4/binary/mvhd.rb +39 -0
  68. data/lib/mp4/binary/nmhd.rb +8 -0
  69. data/lib/mp4/binary/saio.rb +25 -0
  70. data/lib/mp4/binary/saiz.rb +17 -0
  71. data/lib/mp4/binary/sbgp.rb +5 -0
  72. data/lib/mp4/binary/sdtp.rb +18 -0
  73. data/lib/mp4/binary/senc.rb +12 -0
  74. data/lib/mp4/binary/sgpd.rb +5 -0
  75. data/lib/mp4/binary/smhd.rb +11 -0
  76. data/lib/mp4/binary/stco.rb +13 -0
  77. data/lib/mp4/binary/stco_reader.rb +5 -0
  78. data/lib/mp4/binary/sthd.rb +8 -0
  79. data/lib/mp4/binary/stsc.rb +15 -0
  80. data/lib/mp4/binary/stsc_reader.rb +5 -0
  81. data/lib/mp4/binary/stsd.rb +5 -0
  82. data/lib/mp4/binary/stsd_atom.rb +11 -0
  83. data/lib/mp4/binary/stsd_reader.rb +5 -0
  84. data/lib/mp4/binary/stss.rb +13 -0
  85. data/lib/mp4/binary/stss_reader.rb +5 -0
  86. data/lib/mp4/binary/stsz.rb +18 -0
  87. data/lib/mp4/binary/stsz_reader.rb +5 -0
  88. data/lib/mp4/binary/stts.rb +14 -0
  89. data/lib/mp4/binary/stts_reader.rb +5 -0
  90. data/lib/mp4/binary/tkhd.rb +46 -0
  91. data/lib/mp4/binary/udta.rb +5 -0
  92. data/lib/mp4/binary/vmhd.rb +11 -0
  93. data/lib/mp4/cache/memory.rb +328 -0
  94. data/lib/mp4/cache/sqlite.rb +348 -0
  95. data/lib/mp4/cache.rb +13 -0
  96. data/lib/mp4/constants.rb +87 -0
  97. data/lib/mp4/ext/refinements.rb +19 -0
  98. data/lib/mp4/layout/atom.rb +47 -0
  99. data/lib/mp4/layout/base.rb +10 -0
  100. data/lib/mp4/layout/builder.rb +16 -0
  101. data/lib/mp4/layout/container.rb +59 -0
  102. data/lib/mp4/layout/ctts.rb +35 -0
  103. data/lib/mp4/layout/dinf.rb +15 -0
  104. data/lib/mp4/layout/elst.rb +33 -0
  105. data/lib/mp4/layout/free.rb +20 -0
  106. data/lib/mp4/layout/ftyp.rb +21 -0
  107. data/lib/mp4/layout/hdlr.rb +20 -0
  108. data/lib/mp4/layout/mdat.rb +33 -0
  109. data/lib/mp4/layout/mdhd.rb +27 -0
  110. data/lib/mp4/layout/moov.rb +58 -0
  111. data/lib/mp4/layout/mvhd.rb +46 -0
  112. data/lib/mp4/layout/stco.rb +37 -0
  113. data/lib/mp4/layout/stsc.rb +27 -0
  114. data/lib/mp4/layout/stsd.rb +15 -0
  115. data/lib/mp4/layout/stss.rb +22 -0
  116. data/lib/mp4/layout/stsz.rb +39 -0
  117. data/lib/mp4/layout/stts.rb +35 -0
  118. data/lib/mp4/layout/tkhd.rb +45 -0
  119. data/lib/mp4/layout/vsmhd.rb +49 -0
  120. data/lib/mp4/layout.rb +3 -0
  121. data/lib/mp4/metadata/memoize/ftyp.rb +13 -0
  122. data/lib/mp4/metadata/memoize/stbl/binary_stsd.rb +13 -0
  123. data/lib/mp4/metadata/memoize/stbl/chunks.rb +46 -0
  124. data/lib/mp4/metadata/memoize/stbl/codec.rb +82 -0
  125. data/lib/mp4/metadata/memoize/stbl/composition_offset_calculator.rb +31 -0
  126. data/lib/mp4/metadata/memoize/stbl/delta_calculator.rb +29 -0
  127. data/lib/mp4/metadata/memoize/stbl/sample_to_chunk_calculator.rb +63 -0
  128. data/lib/mp4/metadata/memoize/stbl/samples.rb +80 -0
  129. data/lib/mp4/metadata/memoize/track.rb +98 -0
  130. data/lib/mp4/metadata/memoize/tracks.rb +11 -0
  131. data/lib/mp4/metadata/service.rb +30 -0
  132. data/lib/mp4/metadata/summary.rb +11 -0
  133. data/lib/mp4/nolan/service/sound.rb +36 -0
  134. data/lib/mp4/nolan/service/track.rb +9 -0
  135. data/lib/mp4/nolan/service/video.rb +35 -0
  136. data/lib/mp4/nolan/service.rb +84 -0
  137. data/lib/mp4/nolan.rb +3 -0
  138. data/lib/mp4/parser/main_boxes.rb +43 -0
  139. data/lib/mp4/parser/moov.rb +40 -0
  140. data/lib/mp4/parser/service.rb +47 -0
  141. data/lib/mp4/parser/track.rb +56 -0
  142. data/lib/mp4/rb/version.rb +7 -0
  143. data/lib/mp4/rb.rb +10 -0
  144. data/lib/mp4/source/base.rb +20 -0
  145. data/lib/mp4/source/local.rb +79 -0
  146. data/lib/mp4/source/s3.rb +118 -0
  147. data/lib/mp4/source/url.rb +130 -0
  148. data/lib/mp4/source.rb +23 -0
  149. data/lib/mp4/structure/service.rb +13 -0
  150. data/lib/mp4/xlsx/builder.rb +44 -0
  151. data/lib/mp4.rb +222 -0
  152. data/mp4-rb.gemspec +68 -0
  153. data/sig/mp4/rb.rbs +6 -0
  154. data/todo.txt +4 -0
  155. metadata +323 -0
@@ -0,0 +1,328 @@
1
+ module MP4::Cache
2
+ # Drop-in metadata cache backed by Ruby data structures.
3
+ #
4
+ # Implements the **method-style** API (add_track, tracks, chunks, segments,
5
+ # add_sample, each_sample, add_ftyp/ftyp, add_dinf_content/dinf_content,
6
+ # add_stsd_content/stsd_content, add_elst_list_item/elst_entries,
7
+ # add_segment_sample, segment_samples).
8
+ #
9
+ # Deliberately does NOT emulate raw SQL: {#query} and {#execute} raise —
10
+ # see docs/CACHE_MIGRATION.md for the list of call sites (layout writers,
11
+ # Nolan segmentation, DASH manifest) that still speak SQL against
12
+ # MP4::Cache::SQLite and need method-API replacements before this backend
13
+ # can drive the full pipeline.
14
+ class Memory
15
+ def initialize(_location = nil)
16
+ @ftyp = nil
17
+ @tracks = {}
18
+ @chunks = Hash.new { |h, k| h[k] = [] }
19
+ @samples = Hash.new { |h, k| h[k] = [] }
20
+ @segment_samples = Hash.new { |h, k| h[k] = [] }
21
+ @elst = Hash.new { |h, k| h[k] = [] }
22
+ @dinf = {}
23
+ @stsd = {}
24
+ end
25
+
26
+ def transaction
27
+ yield
28
+ end
29
+
30
+ def query(*_args)
31
+ raise NotImplementedError,
32
+ 'MP4::Cache::Memory does not support raw SQL queries. ' \
33
+ 'Use the method-style API instead (see docs/CACHE_MIGRATION.md).'
34
+ end
35
+ alias execute query
36
+
37
+ def add_ftyp(major_brand:, minor_version:, compatible_brands:)
38
+ @ftyp = {
39
+ 'major_brand' => major_brand.to_s,
40
+ 'minor_version' => minor_version.to_i,
41
+ 'compatible_brands' => compatible_brands.to_s,
42
+ }
43
+ end
44
+
45
+ attr_reader :ftyp
46
+
47
+ def add_track(**opts)
48
+ row = {
49
+ 'track_id' => opts.fetch(:track_id),
50
+ 'handler_type' => opts.fetch(:handler_type),
51
+ 'handler_name' => opts.fetch(:handler_name),
52
+ 'have_composition_time' => opts.fetch(:have_composition_time),
53
+ 'timescale' => opts.fetch(:timescale),
54
+ 'creation_time' => opts.fetch(:creation_time),
55
+ 'width' => opts.fetch(:width),
56
+ 'height' => opts.fetch(:height),
57
+ 'volume' => opts.fetch(:volume),
58
+ 'channel_count' => opts.fetch(:channel_count),
59
+ 'par' => opts.fetch(:par),
60
+ 'sar' => opts.fetch(:sar),
61
+ 'codec' => opts.fetch(:codec),
62
+ 'max_bitrate' => opts.fetch(:max_bitrate),
63
+ 'avg_bitrate' => opts.fetch(:avg_bitrate),
64
+ 'sample_rate' => opts.fetch(:sample_rate),
65
+ 'mime_type' => opts.fetch(:mime_type),
66
+ 'alternate_group' => opts.fetch(:alternate_group),
67
+ }
68
+ @tracks[row['track_id']] = row
69
+ end
70
+
71
+ def tracks
72
+ @tracks.values.sort_by { |t| t['handler_type'].to_s }.reverse
73
+ end
74
+
75
+ def tracks_asc
76
+ @tracks.values.sort_by { |t| t['track_id'] }
77
+ end
78
+
79
+ def track(track_id)
80
+ @tracks.fetch(track_id)
81
+ end
82
+
83
+ def add_chunk(track_id:, chunk_index:, samples_per_chunk:, sample_counter:, description_index:, offset:)
84
+ @chunks[track_id] << {
85
+ 'track_id' => track_id,
86
+ 'chunk_index' => chunk_index,
87
+ 'samples_per_chunk' => samples_per_chunk,
88
+ 'sample_counter' => sample_counter,
89
+ 'description_index' => description_index,
90
+ 'offset' => offset,
91
+ }
92
+ end
93
+
94
+ def chunks(track_id)
95
+ @chunks[track_id]
96
+ end
97
+
98
+ def each_track(&block)
99
+ @chunks.keys.uniq.each(&block)
100
+ end
101
+
102
+ def add_sample(track_id:, chunk_index:, sample_index:, chunk_offset:, sample_delta:, delta_sum:, absolute_delta_sum:, composition_offset:, sample_size:, sample_offset: 0, original_chunk_offset: 0, is_keyframe: 'FALSE')
103
+ @samples[track_id] << sample_row(
104
+ track_id: track_id, chunk_index: chunk_index, sample_index: sample_index,
105
+ sample_size: sample_size, sample_offset: sample_offset,
106
+ chunk_offset: chunk_offset, original_chunk_offset: original_chunk_offset,
107
+ sample_delta: sample_delta, delta_sum: delta_sum,
108
+ absolute_delta_sum: absolute_delta_sum,
109
+ composition_offset: composition_offset, is_keyframe: is_keyframe,
110
+ )
111
+ end
112
+
113
+ def each_sample(track_id, &block)
114
+ @samples[track_id].each(&block)
115
+ end
116
+
117
+ def samples(track_id)
118
+ @samples[track_id]
119
+ end
120
+
121
+ def add_segment_sample(track_id:, segment_index:, chunk_index:, sample_index:, chunk_offset:, sample_delta:, delta_sum:, absolute_delta_sum:, composition_offset:, sample_size:, sample_offset: 0, original_chunk_offset: 0, is_keyframe: 'FALSE')
122
+ row = sample_row(
123
+ track_id: track_id, chunk_index: chunk_index, sample_index: sample_index,
124
+ sample_size: sample_size, sample_offset: sample_offset,
125
+ chunk_offset: chunk_offset, original_chunk_offset: original_chunk_offset,
126
+ sample_delta: sample_delta, delta_sum: delta_sum,
127
+ absolute_delta_sum: absolute_delta_sum,
128
+ composition_offset: composition_offset, is_keyframe: is_keyframe,
129
+ )
130
+ row['segment_index'] = segment_index
131
+ @segment_samples[track_id] << row
132
+ end
133
+
134
+ def segments(track_id)
135
+ @segment_samples[track_id]
136
+ .map { |s| s['segment_index'] }
137
+ .uniq
138
+ .sort
139
+ .map { |i| { 'segment_index' => i } }
140
+ end
141
+
142
+ def segment_samples(track_id, segment_index)
143
+ @segment_samples[track_id]
144
+ .select { |s| s['segment_index'] == segment_index }
145
+ .sort_by { |s| s['sample_index'] }
146
+ end
147
+
148
+ def add_elst_list_item(track_id:, track_duration:, media_time:, media_rate:, media_rate_fraction:)
149
+ @elst[track_id] << {
150
+ 'track_id' => track_id,
151
+ 'track_duration' => track_duration,
152
+ 'media_time' => media_time,
153
+ 'media_rate' => media_rate,
154
+ 'media_rate_fraction' => media_rate_fraction,
155
+ }
156
+ end
157
+
158
+ def elst_entries(track_id)
159
+ @elst[track_id]
160
+ end
161
+
162
+ def add_dinf_content(track_id:, content:)
163
+ @dinf[track_id.to_i] = content
164
+ end
165
+
166
+ def dinf_content(track_id)
167
+ @dinf[track_id.to_i]
168
+ end
169
+
170
+ def add_stsd_content(track_id:, content:)
171
+ @stsd[track_id.to_i] = content
172
+ end
173
+
174
+ def stsd_content(track_id)
175
+ @stsd[track_id.to_i]
176
+ end
177
+
178
+ def first_video_track_id
179
+ @tracks.values.select { |t| t['handler_type'] == 'vide' }.map { |t| t['track_id'] }.min
180
+ end
181
+
182
+ def next_track_id
183
+ max = @tracks.keys.max
184
+ max && max + 1
185
+ end
186
+
187
+ def sample_count(track_id)
188
+ @samples[track_id].size
189
+ end
190
+
191
+ def first_sample(track_id)
192
+ @samples[track_id].min_by { |s| s['sample_index'] }
193
+ end
194
+
195
+ def last_sample(track_id)
196
+ @samples[track_id].max_by { |s| s['sample_index'] }
197
+ end
198
+
199
+ def sample_at(track_id, sample_index)
200
+ @samples[track_id].find { |s| s['sample_index'] == sample_index }
201
+ end
202
+
203
+ def sample_delta_sum_ms(track_id, sample_index)
204
+ row = sample_at(track_id, sample_index)
205
+ row && row['delta_sum_millisec']
206
+ end
207
+
208
+ def sum_delta_between(track_id, from_sample_index, to_sample_index)
209
+ @samples[track_id]
210
+ .select { |s| s['sample_index'] >= from_sample_index && s['sample_index'] <= to_sample_index }
211
+ .sum { |s| s['delta'].to_i }
212
+ end
213
+
214
+ def track_sample_delta_sum(track_id)
215
+ @samples[track_id].sum { |s| s['delta'].to_i }
216
+ end
217
+
218
+ def sync_sample_indices(track_id)
219
+ @samples[track_id].select { |s| s['keyframe'] == 'TRUE' }.map { |s| s['sample_index'] }.sort
220
+ end
221
+
222
+ def keyframes_grouped_by_second(track_id)
223
+ buckets = {}
224
+ @samples[track_id].each do |s|
225
+ next unless s['keyframe'] == 'TRUE'
226
+
227
+ bucket = s['delta_sum_millisec'].to_i / 1000
228
+ buckets[bucket] ||= { 'sample_index' => s['sample_index'], 'delta_sum' => s['delta_sum'] }
229
+ end
230
+ buckets.values
231
+ end
232
+
233
+ def samples_between_ms(track_id, from_ms, to_ms)
234
+ @samples[track_id]
235
+ .select { |s| s['delta_sum_millisec'] >= from_ms && s['delta_sum_millisec'] <= to_ms }
236
+ .sort_by { |s| s['sample_index'] }
237
+ end
238
+
239
+ def sample_size_list(track_id)
240
+ @samples[track_id].sort_by { |s| s['sample_index'] }.map { |s| s['size'] }
241
+ end
242
+
243
+ def uniform_sample_size?(track_id)
244
+ @samples[track_id].map { |s| s['size'] }.uniq.size == 1
245
+ end
246
+
247
+ def first_sample_size(track_id)
248
+ first = first_sample(track_id)
249
+ first && first['size']
250
+ end
251
+
252
+ def sample_delta_pairs(track_id)
253
+ @samples[track_id].sort_by { |s| s['sample_index'] }.map { |s| { 'sample_index' => s['sample_index'], 'delta' => s['delta'] } }
254
+ end
255
+
256
+ def sample_c_delta_pairs(track_id)
257
+ @samples[track_id].sort_by { |s| s['sample_index'] }.map { |s| { 'sample_index' => s['sample_index'], 'c_delta' => s['c_delta'] } }
258
+ end
259
+
260
+ def sample_chunk_indices(track_id)
261
+ @samples[track_id].sort_by { |s| s['sample_index'] }.map { |s| s['chunk_index'] }
262
+ end
263
+
264
+ def each_sample_ordered_across_tracks
265
+ rows = @samples.values.flatten.sort_by { |s| [s['sample_index'], s['track_id']] }
266
+ rows.each { |s| yield({ 'track_id' => s['track_id'], 'size' => s['size'], 'offset' => s['offset'] }) }
267
+ end
268
+
269
+ def min_sample_offset
270
+ @samples.values.flatten.map { |s| s['offset'] }.min
271
+ end
272
+
273
+ def segment_duration_ticks(track_id, segment_index)
274
+ @segment_samples[track_id]
275
+ .select { |s| s['segment_index'] == segment_index }
276
+ .sum { |s| s['delta'].to_i }
277
+ end
278
+
279
+ def segment_earliest_presentation_ticks(track_id, segment_index)
280
+ @segment_samples[track_id]
281
+ .select { |s| s['segment_index'] == segment_index }
282
+ .map { |s| s['delta_sum'] }
283
+ .min
284
+ end
285
+
286
+ def segment_size_bytes(track_id, segment_index)
287
+ @segment_samples[track_id]
288
+ .select { |s| s['segment_index'] == segment_index }
289
+ .sum { |s| s['size'].to_i }
290
+ end
291
+
292
+ def segment_duration_ticks_by_index(track_id)
293
+ grouped = @segment_samples[track_id].group_by { |s| s['segment_index'] }
294
+ grouped.map { |i, ss| { 'segment_index' => i, 'duration' => ss.sum { |s| s['delta'].to_i } } }.sort_by { |r| r['segment_index'] }
295
+ end
296
+
297
+ def max_segment_duration_ticks(track_id)
298
+ by_seg = segment_duration_ticks_by_index(track_id)
299
+ by_seg.map { |r| r['duration'] }.max
300
+ end
301
+
302
+ def segment_bytes_sizes_offsets(track_id, segment_index)
303
+ @segment_samples[track_id]
304
+ .select { |s| s['segment_index'] == segment_index }
305
+ .sort_by { |s| s['sample_index'] }
306
+ .map { |s| { 'size' => s['size'], 'offset' => s['offset'] } }
307
+ end
308
+
309
+ private
310
+
311
+ def sample_row(track_id:, chunk_index:, sample_index:, sample_size:, sample_offset:, chunk_offset:, original_chunk_offset:, sample_delta:, delta_sum:, absolute_delta_sum:, composition_offset:, is_keyframe:)
312
+ {
313
+ 'track_id' => track_id,
314
+ 'chunk_index' => chunk_index,
315
+ 'sample_index' => sample_index,
316
+ 'size' => sample_size,
317
+ 'offset' => sample_offset,
318
+ 'chunk_offset' => chunk_offset,
319
+ 'original_chunk_offset' => original_chunk_offset,
320
+ 'delta' => sample_delta,
321
+ 'delta_sum' => delta_sum,
322
+ 'delta_sum_millisec' => absolute_delta_sum,
323
+ 'c_delta' => composition_offset,
324
+ 'keyframe' => is_keyframe,
325
+ }
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,348 @@
1
+ module MP4::Cache
2
+ class SQLite
3
+ def initialize(location = nil)
4
+ require 'sqlite3'
5
+ @cache_db = ::SQLite3::Database.new(location || ':memory:')
6
+ @cache_db.results_as_hash = true
7
+
8
+ load_cache_tables
9
+ end
10
+
11
+ def query(*query)
12
+ query_result = @cache_db.query(*query)
13
+
14
+ if block_given?
15
+ until (row = query_result.next).nil?
16
+ yield(row)
17
+ end
18
+
19
+ return
20
+ end
21
+
22
+ query_result.to_a
23
+ end
24
+
25
+ def execute(*query)
26
+ @cache_db.execute(*query)
27
+ end
28
+
29
+ def transaction(&block)
30
+ @cache_db.transaction(&block)
31
+ end
32
+
33
+ def add_ftyp(major_brand:, minor_version:, compatible_brands:)
34
+ execute(
35
+ 'INSERT INTO ftyp_cache VALUES(?, ?, ?)',
36
+ major_brand.to_s, minor_version.to_i, compatible_brands.to_s,
37
+ )
38
+ end
39
+
40
+ def ftyp
41
+ row = query('SELECT * FROM ftyp_cache').first
42
+ row && {
43
+ 'major_brand' => row['major_brand'],
44
+ 'minor_version' => row['minor_version'],
45
+ 'compatible_brands' => row['compatible_brands'],
46
+ }
47
+ end
48
+
49
+ def add_chunk(track_id:, chunk_index:, samples_per_chunk:, sample_counter:, description_index:, offset:)
50
+ execute(
51
+ 'INSERT INTO chunk_cache VALUES(?, ?, ?, ?, ?, ?)',
52
+ track_id, chunk_index, samples_per_chunk, sample_counter, description_index, offset,
53
+ )
54
+ end
55
+
56
+ def add_dinf_content(track_id:, content:)
57
+ execute('INSERT INTO dinf_cache VALUES(?, ?)', track_id.to_i, content)
58
+ end
59
+
60
+ def dinf_content(track_id)
61
+ query('SELECT content FROM dinf_cache WHERE track_id = ?', track_id).dig(0, 'content')
62
+ end
63
+
64
+ def add_stsd_content(track_id:, content:)
65
+ execute('INSERT INTO stsd_cache VALUES(?, ?)', track_id.to_i, content)
66
+ end
67
+
68
+ def stsd_content(track_id)
69
+ query('SELECT content FROM stsd_cache WHERE track_id = ?', track_id).dig(0, 'content')
70
+ end
71
+
72
+ def elst_entries(track_id)
73
+ query('SELECT * FROM elst_cache WHERE track_id = ?', track_id)
74
+ end
75
+
76
+ def add_track(**opts)
77
+ insert_sql = <<-SQL
78
+ INSERT INTO tracks_cache VALUES
79
+ (
80
+ :track_id,
81
+ :handler_type,
82
+ :handler_name,
83
+ :have_composition_time,
84
+ :timescale,
85
+ :creation_time,
86
+ :width,
87
+ :height,
88
+ :volume,
89
+ :channel_count,
90
+ :par,
91
+ :sar,
92
+ :codec,
93
+ :max_bitrate,
94
+ :avg_bitrate,
95
+ :sample_rate,
96
+ :mime_type,
97
+ :alternate_group
98
+ );
99
+ SQL
100
+
101
+ self.execute(insert_sql, opts)
102
+ end
103
+
104
+ def add_segment_sample(track_id:, segment_index:, chunk_index:, sample_index:, **opts)
105
+ insert_sql = <<-SQL
106
+ INSERT INTO segments_cache VALUES(
107
+ ?, -- track_id
108
+ ?, -- segment_index
109
+ ?, -- chunk_index
110
+ ?, -- sample_index
111
+ ?, -- sample_size
112
+ ?, -- sample_offset
113
+ ?, -- chunk_offset
114
+ ?, -- original_chunk_offset
115
+ ?, -- sample_delta
116
+ ?, -- delta_sum
117
+ ?, -- delta_sum_millsec
118
+ ?, -- c_delta
119
+ ? -- is_keyframe
120
+ );
121
+ SQL
122
+
123
+ self.execute(
124
+ insert_sql,
125
+ track_id,
126
+ segment_index,
127
+ chunk_index,
128
+ sample_index,
129
+ opts.fetch(:sample_size),
130
+ opts.fetch(:sample_offset, 0),
131
+ opts.fetch(:chunk_offset),
132
+ opts.fetch(:original_chunk_offset, 0),
133
+ opts.fetch(:sample_delta),
134
+ opts.fetch(:delta_sum),
135
+ opts.fetch(:absolute_delta_sum),
136
+ opts.fetch(:composition_offset),
137
+ opts.fetch(:is_keyframe, 'FALSE'),
138
+ )
139
+ end
140
+
141
+ def add_elst_list_item(**opts)
142
+ insert_sql = <<-SQL
143
+ INSERT INTO elst_cache VALUES(
144
+ :track_id,
145
+ :track_duration,
146
+ :media_time,
147
+ :media_rate,
148
+ :media_rate_fraction
149
+ );
150
+ SQL
151
+
152
+ self.execute(
153
+ insert_sql,
154
+ opts.fetch(:track_id),
155
+ opts.fetch(:track_duration),
156
+ opts.fetch(:media_time),
157
+ opts.fetch(:media_rate),
158
+ opts.fetch(:media_rate_fraction),
159
+ )
160
+ end
161
+
162
+ def add_sample(track_id:, chunk_index:, sample_index:, **opts)
163
+ insert_sql = <<-SQL
164
+ INSERT INTO samples_cache VALUES(
165
+ ?, -- track_id
166
+ ?, -- chunk_index
167
+ ?, -- sample_index
168
+ ?, -- sample_size
169
+ ?, -- sample_offset
170
+ ?, -- chunk_offset
171
+ ?, -- original_chunk_offset
172
+ ?, -- sample_delta
173
+ ?, -- delta_sum
174
+ ?, -- delta_sum_millsec
175
+ ?, -- c_delta
176
+ ? -- is_keyframe
177
+ );
178
+ SQL
179
+
180
+ self.execute(
181
+ insert_sql,
182
+ track_id,
183
+ chunk_index,
184
+ sample_index,
185
+ opts.fetch(:sample_size),
186
+ opts.fetch(:sample_offset, 0),
187
+ opts.fetch(:chunk_offset),
188
+ opts.fetch(:original_chunk_offset, 0),
189
+ opts.fetch(:sample_delta),
190
+ opts.fetch(:delta_sum),
191
+ opts.fetch(:absolute_delta_sum),
192
+ opts.fetch(:composition_offset),
193
+ opts.fetch(:is_keyframe, 'FALSE'),
194
+ )
195
+ end
196
+
197
+ def tracks_asc
198
+ query('SELECT * FROM tracks_cache ORDER BY track_id ASC')
199
+ end
200
+
201
+ def tracks
202
+ query('SELECT * FROM tracks_cache ORDER BY handler_type DESC')
203
+ end
204
+
205
+ def track(track_id)
206
+ query('SELECT * FROM tracks_cache WHERE track_id = ?', track_id).fetch(0)
207
+ end
208
+
209
+ def segments(track_id)
210
+ query('SELECT segment_index FROM segments_cache WHERE track_id = ? GROUP BY segment_index ORDER BY segment_index ASC', track_id)
211
+ end
212
+
213
+ def segment_samples(track_id, segment_index)
214
+ query('SELECT * FROM segments_cache WHERE track_id = ? AND segment_index = ? ORDER BY sample_index ASC', track_id, segment_index)
215
+ end
216
+
217
+ def chunks(track_id)
218
+ query('select * from chunk_cache where track_id = ?', track_id)
219
+ end
220
+
221
+ def each_track
222
+ query('select DISTINCT track_id from chunk_cache') do |row|
223
+ yield(row.fetch('track_id'))
224
+ end
225
+ end
226
+
227
+ def each_sample(track_id, &block)
228
+ query('select * from samples_cache where track_id = ?', track_id, &block)
229
+ end
230
+
231
+ def first_video_track_id
232
+ query('SELECT track_id FROM tracks_cache WHERE handler_type = ? ORDER BY track_id ASC LIMIT 1', 'vide').dig(0, 'track_id')
233
+ end
234
+
235
+ def next_track_id
236
+ query('SELECT MAX(track_id) + 1 AS n FROM tracks_cache').dig(0, 'n')
237
+ end
238
+
239
+ def sample_count(track_id)
240
+ query('SELECT COUNT(1) AS c FROM samples_cache WHERE track_id = ?', track_id).dig(0, 'c')
241
+ end
242
+
243
+ def first_sample(track_id)
244
+ query('SELECT * FROM samples_cache WHERE track_id = ? ORDER BY sample_index ASC LIMIT 1', track_id).first
245
+ end
246
+
247
+ def last_sample(track_id)
248
+ query('SELECT * FROM samples_cache WHERE track_id = ? ORDER BY sample_index DESC LIMIT 1', track_id).first
249
+ end
250
+
251
+ def sample_at(track_id, sample_index)
252
+ query('SELECT * FROM samples_cache WHERE track_id = ? AND sample_index = ? LIMIT 1', track_id, sample_index).first
253
+ end
254
+
255
+ def sample_delta_sum_ms(track_id, sample_index)
256
+ query('SELECT delta_sum_millisec FROM samples_cache WHERE track_id = ? AND sample_index = ? LIMIT 1', track_id, sample_index).dig(0, 'delta_sum_millisec')
257
+ end
258
+
259
+ def sum_delta_between(track_id, from_sample_index, to_sample_index)
260
+ query('SELECT SUM(delta) AS s FROM samples_cache WHERE track_id = ? AND sample_index >= ? AND sample_index <= ?', track_id, from_sample_index, to_sample_index).dig(0, 's')
261
+ end
262
+
263
+ def track_sample_delta_sum(track_id)
264
+ query('SELECT SUM(delta) AS s FROM samples_cache WHERE track_id = ?', track_id).dig(0, 's').to_i
265
+ end
266
+
267
+ def sync_sample_indices(track_id)
268
+ query("SELECT sample_index FROM samples_cache WHERE track_id = ? AND keyframe = 'TRUE' ORDER BY sample_index ASC", track_id).map { |r| r.fetch('sample_index') }
269
+ end
270
+
271
+ def keyframes_grouped_by_second(track_id)
272
+ query("SELECT sample_index, delta_sum FROM samples_cache WHERE track_id = ? AND keyframe = 'TRUE' GROUP BY (delta_sum_millisec / 1000)", track_id)
273
+ end
274
+
275
+ def samples_between_ms(track_id, from_ms, to_ms)
276
+ query('SELECT * FROM samples_cache WHERE track_id = ? AND delta_sum_millisec >= ? AND delta_sum_millisec <= ? ORDER BY sample_index ASC', track_id, from_ms, to_ms)
277
+ end
278
+
279
+ def sample_size_list(track_id)
280
+ query('SELECT size FROM samples_cache WHERE track_id = ? ORDER BY sample_index ASC', track_id).map { |r| r.fetch('size') }
281
+ end
282
+
283
+ def uniform_sample_size?(track_id)
284
+ query('SELECT COUNT(1) AS c FROM samples_cache WHERE track_id = ? GROUP BY size LIMIT 2', track_id).size == 1
285
+ end
286
+
287
+ def first_sample_size(track_id)
288
+ query('SELECT size FROM samples_cache WHERE track_id = ? ORDER BY sample_index ASC LIMIT 1', track_id).dig(0, 'size')
289
+ end
290
+
291
+ def sample_delta_pairs(track_id)
292
+ query('SELECT sample_index, delta FROM samples_cache WHERE track_id = ? ORDER BY sample_index ASC', track_id)
293
+ end
294
+
295
+ def sample_c_delta_pairs(track_id)
296
+ query('SELECT sample_index, c_delta FROM samples_cache WHERE track_id = ? ORDER BY sample_index ASC', track_id)
297
+ end
298
+
299
+ def sample_chunk_indices(track_id)
300
+ query('SELECT chunk_index FROM samples_cache WHERE track_id = ? ORDER BY sample_index ASC', track_id).map { |r| r.fetch('chunk_index') }
301
+ end
302
+
303
+ def each_sample_ordered_across_tracks(&block)
304
+ query('SELECT track_id, size, offset FROM samples_cache ORDER BY sample_index ASC, track_id ASC', &block)
305
+ end
306
+
307
+ def min_sample_offset
308
+ query('SELECT MIN(offset) AS o FROM samples_cache').dig(0, 'o')
309
+ end
310
+
311
+ def segment_duration_ticks(track_id, segment_index)
312
+ query('SELECT SUM(delta) AS d FROM segments_cache WHERE track_id = ? AND segment_index = ?', track_id, segment_index).dig(0, 'd')
313
+ end
314
+
315
+ def segment_earliest_presentation_ticks(track_id, segment_index)
316
+ query('SELECT MIN(delta_sum) AS d FROM segments_cache WHERE track_id = ? AND segment_index = ?', track_id, segment_index).dig(0, 'd')
317
+ end
318
+
319
+ def segment_size_bytes(track_id, segment_index)
320
+ query('SELECT SUM(size) AS s FROM segments_cache WHERE track_id = ? AND segment_index = ?', track_id, segment_index).dig(0, 's')
321
+ end
322
+
323
+ def segment_duration_ticks_by_index(track_id)
324
+ query('SELECT segment_index, SUM(delta) AS duration FROM segments_cache WHERE track_id = ? GROUP BY segment_index ORDER BY segment_index ASC', track_id)
325
+ end
326
+
327
+ def max_segment_duration_ticks(track_id)
328
+ query('SELECT SUM(delta) AS d FROM segments_cache WHERE track_id = ? GROUP BY segment_index ORDER BY d DESC LIMIT 1', track_id).dig(0, 'd')
329
+ end
330
+
331
+ def segment_bytes_sizes_offsets(track_id, segment_index)
332
+ query('SELECT size, offset FROM segments_cache WHERE track_id = ? AND segment_index = ? ORDER BY sample_index ASC', track_id, segment_index)
333
+ end
334
+
335
+ private
336
+
337
+ def load_cache_tables
338
+ migration_files.each do |migration_file|
339
+ execute migration_file.read
340
+ end
341
+ end
342
+
343
+ def migration_files
344
+ migration_dir = ::MP4.root.join('migrations')
345
+ Dir.entries(migration_dir).select { |path| path.end_with?('.sql') }.map { |path| Pathname.new(migration_dir.join(path)) }
346
+ end
347
+ end
348
+ end
data/lib/mp4/cache.rb ADDED
@@ -0,0 +1,13 @@
1
+ module MP4::Cache
2
+ BACKENDS = {
3
+ sqlite: 'MP4::Cache::SQLite',
4
+ memory: 'MP4::Cache::Memory',
5
+ }.freeze
6
+
7
+ def self.build(backend, location = nil)
8
+ klass_name = BACKENDS.fetch(backend) do
9
+ raise ArgumentError, "unknown cache backend: #{backend.inspect} (known: #{BACKENDS.keys.inspect})"
10
+ end
11
+ Object.const_get(klass_name).new(location)
12
+ end
13
+ end