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.
- checksums.yaml +7 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +149 -0
- data/README.md +195 -0
- data/Rakefile +8 -0
- data/bin/build_mp4_xlsx.rb +35 -0
- data/bin/divide_mp4_to_segments.rb +52 -0
- data/lib/m4s/binary/ftyp.rb +11 -0
- data/lib/m4s/binary/mdat.rb +0 -0
- data/lib/m4s/binary/mfhd.rb +18 -0
- data/lib/m4s/binary/mvhd.rb +39 -0
- data/lib/m4s/binary/sidx.rb +38 -0
- data/lib/m4s/binary/styp.rb +17 -0
- data/lib/m4s/binary/tfdt.rb +25 -0
- data/lib/m4s/binary/tfhd.rb +38 -0
- data/lib/m4s/binary/trex.rb +25 -0
- data/lib/m4s/binary/trun.rb +30 -0
- data/lib/m4s/dash/manifest.rb +149 -0
- data/lib/m4s/layout/atom.rb +44 -0
- data/lib/m4s/layout/builder.rb +32 -0
- data/lib/m4s/layout/container.rb +59 -0
- data/lib/m4s/layout/ctts.rb +20 -0
- data/lib/m4s/layout/dinf.rb +17 -0
- data/lib/m4s/layout/elst.rb +33 -0
- data/lib/m4s/layout/ftyp.rb +21 -0
- data/lib/m4s/layout/hdlr.rb +22 -0
- data/lib/m4s/layout/init_mp4_builder.rb +57 -0
- data/lib/m4s/layout/mdat.rb +61 -0
- data/lib/m4s/layout/mdhd.rb +25 -0
- data/lib/m4s/layout/mfhd.rb +17 -0
- data/lib/m4s/layout/mvhd.rb +32 -0
- data/lib/m4s/layout/sidx.rb +36 -0
- data/lib/m4s/layout/stco.rb +21 -0
- data/lib/m4s/layout/stsc.rb +20 -0
- data/lib/m4s/layout/stsd.rb +17 -0
- data/lib/m4s/layout/stss.rb +20 -0
- data/lib/m4s/layout/stsz.rb +19 -0
- data/lib/m4s/layout/stts.rb +20 -0
- data/lib/m4s/layout/styp.rb +17 -0
- data/lib/m4s/layout/tfdt.rb +21 -0
- data/lib/m4s/layout/tfhd.rb +17 -0
- data/lib/m4s/layout/tkhd.rb +34 -0
- data/lib/m4s/layout/trex.rb +33 -0
- data/lib/m4s/layout/trun.rb +89 -0
- data/lib/m4s/layout/vsmhd.rb +51 -0
- data/lib/m4s/m3u8/general_playlist.rb +52 -0
- data/lib/m4s/m3u8/media_playlist.rb +32 -0
- data/lib/mp4/binary/avc1.rb +98 -0
- data/lib/mp4/binary/box_head.rb +8 -0
- data/lib/mp4/binary/co64.rb +11 -0
- data/lib/mp4/binary/codec.rb +8 -0
- data/lib/mp4/binary/ctts.rb +14 -0
- data/lib/mp4/binary/ctts_reader.rb +5 -0
- data/lib/mp4/binary/dinf.rb +5 -0
- data/lib/mp4/binary/dref.rb +5 -0
- data/lib/mp4/binary/elst.rb +16 -0
- data/lib/mp4/binary/fixed_point16.rb +19 -0
- data/lib/mp4/binary/fixed_point8.rb +19 -0
- data/lib/mp4/binary/ftyp.rb +9 -0
- data/lib/mp4/binary/gmhd.rb +5 -0
- data/lib/mp4/binary/hdlr.rb +13 -0
- data/lib/mp4/binary/hev1.rb +73 -0
- data/lib/mp4/binary/hmhd.rb +14 -0
- data/lib/mp4/binary/iods.rb +5 -0
- data/lib/mp4/binary/mdhd.rb +18 -0
- data/lib/mp4/binary/mp4a.rb +76 -0
- data/lib/mp4/binary/mvhd.rb +39 -0
- data/lib/mp4/binary/nmhd.rb +8 -0
- data/lib/mp4/binary/saio.rb +25 -0
- data/lib/mp4/binary/saiz.rb +17 -0
- data/lib/mp4/binary/sbgp.rb +5 -0
- data/lib/mp4/binary/sdtp.rb +18 -0
- data/lib/mp4/binary/senc.rb +12 -0
- data/lib/mp4/binary/sgpd.rb +5 -0
- data/lib/mp4/binary/smhd.rb +11 -0
- data/lib/mp4/binary/stco.rb +13 -0
- data/lib/mp4/binary/stco_reader.rb +5 -0
- data/lib/mp4/binary/sthd.rb +8 -0
- data/lib/mp4/binary/stsc.rb +15 -0
- data/lib/mp4/binary/stsc_reader.rb +5 -0
- data/lib/mp4/binary/stsd.rb +5 -0
- data/lib/mp4/binary/stsd_atom.rb +11 -0
- data/lib/mp4/binary/stsd_reader.rb +5 -0
- data/lib/mp4/binary/stss.rb +13 -0
- data/lib/mp4/binary/stss_reader.rb +5 -0
- data/lib/mp4/binary/stsz.rb +18 -0
- data/lib/mp4/binary/stsz_reader.rb +5 -0
- data/lib/mp4/binary/stts.rb +14 -0
- data/lib/mp4/binary/stts_reader.rb +5 -0
- data/lib/mp4/binary/tkhd.rb +46 -0
- data/lib/mp4/binary/udta.rb +5 -0
- data/lib/mp4/binary/vmhd.rb +11 -0
- data/lib/mp4/cache/memory.rb +328 -0
- data/lib/mp4/cache/sqlite.rb +348 -0
- data/lib/mp4/cache.rb +13 -0
- data/lib/mp4/constants.rb +87 -0
- data/lib/mp4/ext/refinements.rb +19 -0
- data/lib/mp4/layout/atom.rb +47 -0
- data/lib/mp4/layout/base.rb +10 -0
- data/lib/mp4/layout/builder.rb +16 -0
- data/lib/mp4/layout/container.rb +59 -0
- data/lib/mp4/layout/ctts.rb +35 -0
- data/lib/mp4/layout/dinf.rb +15 -0
- data/lib/mp4/layout/elst.rb +33 -0
- data/lib/mp4/layout/free.rb +20 -0
- data/lib/mp4/layout/ftyp.rb +21 -0
- data/lib/mp4/layout/hdlr.rb +20 -0
- data/lib/mp4/layout/mdat.rb +33 -0
- data/lib/mp4/layout/mdhd.rb +27 -0
- data/lib/mp4/layout/moov.rb +58 -0
- data/lib/mp4/layout/mvhd.rb +46 -0
- data/lib/mp4/layout/stco.rb +37 -0
- data/lib/mp4/layout/stsc.rb +27 -0
- data/lib/mp4/layout/stsd.rb +15 -0
- data/lib/mp4/layout/stss.rb +22 -0
- data/lib/mp4/layout/stsz.rb +39 -0
- data/lib/mp4/layout/stts.rb +35 -0
- data/lib/mp4/layout/tkhd.rb +45 -0
- data/lib/mp4/layout/vsmhd.rb +49 -0
- data/lib/mp4/layout.rb +3 -0
- data/lib/mp4/metadata/memoize/ftyp.rb +13 -0
- data/lib/mp4/metadata/memoize/stbl/binary_stsd.rb +13 -0
- data/lib/mp4/metadata/memoize/stbl/chunks.rb +46 -0
- data/lib/mp4/metadata/memoize/stbl/codec.rb +82 -0
- data/lib/mp4/metadata/memoize/stbl/composition_offset_calculator.rb +31 -0
- data/lib/mp4/metadata/memoize/stbl/delta_calculator.rb +29 -0
- data/lib/mp4/metadata/memoize/stbl/sample_to_chunk_calculator.rb +63 -0
- data/lib/mp4/metadata/memoize/stbl/samples.rb +80 -0
- data/lib/mp4/metadata/memoize/track.rb +98 -0
- data/lib/mp4/metadata/memoize/tracks.rb +11 -0
- data/lib/mp4/metadata/service.rb +30 -0
- data/lib/mp4/metadata/summary.rb +11 -0
- data/lib/mp4/nolan/service/sound.rb +36 -0
- data/lib/mp4/nolan/service/track.rb +9 -0
- data/lib/mp4/nolan/service/video.rb +35 -0
- data/lib/mp4/nolan/service.rb +84 -0
- data/lib/mp4/nolan.rb +3 -0
- data/lib/mp4/parser/main_boxes.rb +43 -0
- data/lib/mp4/parser/moov.rb +40 -0
- data/lib/mp4/parser/service.rb +47 -0
- data/lib/mp4/parser/track.rb +56 -0
- data/lib/mp4/rb/version.rb +7 -0
- data/lib/mp4/rb.rb +10 -0
- data/lib/mp4/source/base.rb +20 -0
- data/lib/mp4/source/local.rb +79 -0
- data/lib/mp4/source/s3.rb +118 -0
- data/lib/mp4/source/url.rb +130 -0
- data/lib/mp4/source.rb +23 -0
- data/lib/mp4/structure/service.rb +13 -0
- data/lib/mp4/xlsx/builder.rb +44 -0
- data/lib/mp4.rb +222 -0
- data/mp4-rb.gemspec +68 -0
- data/sig/mp4/rb.rbs +6 -0
- data/todo.txt +4 -0
- 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
|