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,39 @@
|
|
|
1
|
+
module MP4::Layout
|
|
2
|
+
class Stsz < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def name
|
|
8
|
+
'stsz'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def binary_content
|
|
12
|
+
return @binary_content if defined?(@binary_content)
|
|
13
|
+
|
|
14
|
+
binary_content = if all_sizes_equeal?
|
|
15
|
+
MP4::Binary::Stsz.new(sample_size: sample_size, sample_count: sample_count)
|
|
16
|
+
else
|
|
17
|
+
MP4::Binary::Stsz.new(sample_count: sample_size_list.size, sample_sizes: sample_size_list)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@binary_content = binary_content.to_binary_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def sample_size_list
|
|
24
|
+
@sample_size_list ||= media_cache.sample_size_list(track_id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def all_sizes_equeal?
|
|
28
|
+
media_cache.uniform_sample_size?(track_id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sample_size
|
|
32
|
+
media_cache.first_sample_size(track_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sample_count
|
|
36
|
+
media_cache.sample_count(track_id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module MP4::Layout
|
|
2
|
+
class Stts < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def name
|
|
8
|
+
'stts'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def binary_content
|
|
12
|
+
@binary_content ||= MP4::Binary::Stts.new(
|
|
13
|
+
entries_count: generate_time_to_sample_list.size,
|
|
14
|
+
time_to_sample: generate_time_to_sample_list,
|
|
15
|
+
).to_binary_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_time_to_sample_list
|
|
19
|
+
return @generate_time_to_sample_list if defined?(@generate_time_to_sample_list)
|
|
20
|
+
|
|
21
|
+
buffer = []
|
|
22
|
+
media_cache.sample_delta_pairs(track_id).each do |row|
|
|
23
|
+
delta = row.fetch('delta')
|
|
24
|
+
latest = buffer.last
|
|
25
|
+
if latest && latest[:sample_delta] == delta
|
|
26
|
+
latest[:sample_count] += 1
|
|
27
|
+
else
|
|
28
|
+
buffer << { sample_count: 1, sample_delta: delta }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@generate_time_to_sample_list = buffer
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module MP4::Layout
|
|
2
|
+
class Tkhd < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def name
|
|
8
|
+
'tkhd'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def binary_content
|
|
12
|
+
@binary_content ||= MP4::Binary::Tkhd.new(
|
|
13
|
+
version: 0,
|
|
14
|
+
flags: [0, 0, 3],
|
|
15
|
+
creation_time: 0,
|
|
16
|
+
modification_time: 0,
|
|
17
|
+
track_id: track_id,
|
|
18
|
+
duration: calculate_duration,
|
|
19
|
+
layer: 0,
|
|
20
|
+
alternate_group: 0,
|
|
21
|
+
volume: get_volume,
|
|
22
|
+
matrix: [65536, 0, 0, 0, 65536, 0, 0, 0, 1073741824],
|
|
23
|
+
width: get_width,
|
|
24
|
+
height: get_height,
|
|
25
|
+
).to_binary_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def calculate_duration
|
|
29
|
+
seconds = media_cache.track_sample_delta_sum(track_id) / track.fetch('timescale').to_f
|
|
30
|
+
(seconds.round(3) * MP4::Layout::Mvhd::DEFAULT_TIMESCALE).to_i
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_volume
|
|
34
|
+
track.fetch('volume').to_f
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_width
|
|
38
|
+
track.fetch('width').to_f
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_height
|
|
42
|
+
track.fetch('height').to_f
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module MP4::Layout
|
|
2
|
+
class VSmhd < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def name
|
|
8
|
+
return 'vmhd' if video?
|
|
9
|
+
return 'smhd' if sound?
|
|
10
|
+
|
|
11
|
+
raise NotImplementedError, ''
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def binary_content
|
|
15
|
+
return @binary_content if defined?(@binary_content)
|
|
16
|
+
|
|
17
|
+
serializer_klass = if video?
|
|
18
|
+
::MP4::Binary::Vmhd
|
|
19
|
+
elsif sound?
|
|
20
|
+
::MP4::Binary::Smhd
|
|
21
|
+
else
|
|
22
|
+
raise "Unknown track handler type #{track.fetch('handler_type')}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@binary_content = serializer_klass.new(handler_attrs).to_binary_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def handler_attrs
|
|
29
|
+
if video?
|
|
30
|
+
{
|
|
31
|
+
flags: [0, 0, 0],
|
|
32
|
+
opcolor: [0, 0, 0],
|
|
33
|
+
}
|
|
34
|
+
elsif sound?
|
|
35
|
+
{
|
|
36
|
+
flags: [0, 0, 0],
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def video?
|
|
42
|
+
@video ||= track.fetch('handler_type') == 'vide'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sound?
|
|
46
|
+
@sound ||= track.fetch('handler_type') == 'soun'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/mp4/layout.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Ftyp
|
|
4
|
+
def self.call(cache, ftyp)
|
|
5
|
+
cache.add_ftyp(
|
|
6
|
+
major_brand: ftyp.major_brand.to_s,
|
|
7
|
+
minor_version: ftyp.minor_version.to_i,
|
|
8
|
+
compatible_brands: ftyp.compatible_brands.to_s,
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Stbl
|
|
4
|
+
module Chunks
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(track_id, cache, stco, stsc)
|
|
8
|
+
each_chunk(stsc, stco) do |chunk_index, samples_per_chunk, offset, description_index, sample_counter|
|
|
9
|
+
cache.add_chunk(
|
|
10
|
+
track_id: track_id,
|
|
11
|
+
chunk_index: chunk_index,
|
|
12
|
+
samples_per_chunk: samples_per_chunk,
|
|
13
|
+
sample_counter: sample_counter,
|
|
14
|
+
description_index: description_index,
|
|
15
|
+
offset: offset,
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each_chunk(stsc, stco)
|
|
21
|
+
sample_counter = 0
|
|
22
|
+
|
|
23
|
+
stsc.chunk_count.times do |i|
|
|
24
|
+
first_chunk = stsc.table[i].first_chunk.to_i
|
|
25
|
+
samples_per_chunk = stsc.table[i].samples_per_chunk.to_i
|
|
26
|
+
description_index = stsc.table[i].sample_description_index.to_i
|
|
27
|
+
|
|
28
|
+
last_chunk = i.next < stsc.chunk_count ? stsc.table[i.next].first_chunk - 1 : stco.chunks_count
|
|
29
|
+
|
|
30
|
+
(first_chunk..last_chunk).each do |chunk_index|
|
|
31
|
+
sample_counter += samples_per_chunk
|
|
32
|
+
|
|
33
|
+
yield(
|
|
34
|
+
chunk_index,
|
|
35
|
+
samples_per_chunk,
|
|
36
|
+
stco.offsets[chunk_index.pred].to_i,
|
|
37
|
+
description_index,
|
|
38
|
+
sample_counter
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Stbl
|
|
4
|
+
module Codec
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def parse(stsd_cache)
|
|
8
|
+
stsd = MP4::Binary::StsdAtom.read(stsd_cache)
|
|
9
|
+
|
|
10
|
+
memoize_codecs(stsd.codecs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def memoize_codecs(codecs)
|
|
14
|
+
codecs.map { extract_codec(_1) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extract_codec(codec)
|
|
18
|
+
case codec.codec_type
|
|
19
|
+
when 'avc1'
|
|
20
|
+
avc1_data = MP4::Binary::Avc1.read(codec.content)
|
|
21
|
+
avc1(avc1_data)
|
|
22
|
+
when 'mp4a'
|
|
23
|
+
mp4a_data = MP4::Binary::Mp4a.read(codec.content)
|
|
24
|
+
mp4a(mp4a_data)
|
|
25
|
+
when 'hev1'
|
|
26
|
+
hev1_data = MP4::Binary::Hev1.read(codec.content)
|
|
27
|
+
hev1(hev1_data)
|
|
28
|
+
else
|
|
29
|
+
raise NotImplementedError, "Unable to memoize `#{codec.codec_type}' codec, track #{track_id}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def avc1(avc1_data)
|
|
34
|
+
avc_profile_indication = avc1_data.avcC.avc_profile_indication
|
|
35
|
+
profile_compatibility = avc1_data.avcC.profile_compatibility
|
|
36
|
+
avc_level_indication = avc1_data.avcC.avc_level_indication
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
par: calculate_aspect_ratio(avc1_data.width.to_i, avc1_data.height.to_i),
|
|
40
|
+
sar: "#{avc1_data.pasp.h_spacing}:#{avc1_data.pasp.v_spacing}",
|
|
41
|
+
codec: format("avc1.%02X%02X%02X", avc_profile_indication, profile_compatibility, avc_level_indication).downcase,
|
|
42
|
+
max_bitrate: avc1_data.bitrate.max_bitrate.to_i,
|
|
43
|
+
avg_bitrate: avc1_data.bitrate.avg_bitrate.to_i,
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mp4a(mp4a_data)
|
|
48
|
+
object_type_indication = mp4a_data.esds.es_descriptor.dec_config_desc.object_type_indication
|
|
49
|
+
audio_object_type_byte = mp4a_data.esds.es_descriptor.dec_config_desc.dec_specific_info.info.bytes[0]
|
|
50
|
+
audio_object_type = (audio_object_type_byte >> 3) & 0x1F
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
codec: format("mp4a.%02x.%d", object_type_indication, audio_object_type),
|
|
54
|
+
channel_count: mp4a_data.channel_count,
|
|
55
|
+
sample_rate: (mp4a_data.sample_rate >> 16).to_i,
|
|
56
|
+
max_bitrate: mp4a_data.esds.es_descriptor.dec_config_desc.max_bitrate.to_i,
|
|
57
|
+
avg_bitrate: mp4a_data.esds.es_descriptor.dec_config_desc.avg_bitrate.to_i,
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hev1(hev1_data)
|
|
62
|
+
{
|
|
63
|
+
par: calculate_aspect_ratio(hev1_data[:width].to_i, hev1_data[:height].to_i),
|
|
64
|
+
sar: "#{hev1_data[:pasp].h_spacing}:#{hev1_data[:pasp].v_spacing}",
|
|
65
|
+
codec: 'hev1',
|
|
66
|
+
max_bitrate: hev1_data[:bitrate].max_bitrate.to_i,
|
|
67
|
+
avg_bitrate: hev1_data[:bitrate].avg_bitrate.to_i,
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private_class_method def calculate_aspect_ratio(width, height)
|
|
72
|
+
gcd = width.gcd(height)
|
|
73
|
+
|
|
74
|
+
aspect_ratio_width = width / gcd
|
|
75
|
+
aspect_ratio_height = height / gcd
|
|
76
|
+
|
|
77
|
+
"#{aspect_ratio_width}:#{aspect_ratio_height}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Stbl
|
|
4
|
+
class CompositionOffsetCalculator
|
|
5
|
+
def initialize(ctts_table)
|
|
6
|
+
@ctts_table = ctts_table.dup
|
|
7
|
+
@ctts_table_dup = ctts_table.dup
|
|
8
|
+
@cached_total_samples = 0
|
|
9
|
+
@cached_composition_offset = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def find_composition_offset(sample_number)
|
|
13
|
+
return nil if @ctts_table.nil?
|
|
14
|
+
|
|
15
|
+
return @cached_composition_offset if sample_number <= @cached_total_samples
|
|
16
|
+
|
|
17
|
+
while (entry = @ctts_table.shift)
|
|
18
|
+
@cached_total_samples += entry.sample_count
|
|
19
|
+
|
|
20
|
+
if sample_number <= @cached_total_samples
|
|
21
|
+
@cached_composition_offset = entry.fetch(:composition_offset)
|
|
22
|
+
return @cached_composition_offset
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
raise "Sample Composition offset not found for sample index: #{sample_number}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Stbl
|
|
4
|
+
class DeltaCalculator
|
|
5
|
+
def initialize(stts_table)
|
|
6
|
+
@stts_table = stts_table.dup
|
|
7
|
+
@stts_table_dup = stts_table.dup
|
|
8
|
+
@samples_processed_count = 0
|
|
9
|
+
@cached_delta = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def find_delta(sample_number)
|
|
13
|
+
return @cached_delta if sample_number <= @samples_processed_count
|
|
14
|
+
|
|
15
|
+
while (entry = @stts_table.shift)
|
|
16
|
+
@samples_processed_count += entry.sample_count
|
|
17
|
+
|
|
18
|
+
if sample_number <= @samples_processed_count
|
|
19
|
+
@cached_delta = entry.fetch(:sample_delta)
|
|
20
|
+
return @cached_delta
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
raise "Delta not found for sample index: #{sample_number}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Stbl
|
|
4
|
+
class SampleToChunkCalculator
|
|
5
|
+
def initialize(stsc_table, total_chunks_count)
|
|
6
|
+
@stsc_table = stsc_table.dup
|
|
7
|
+
@total_chunks_count = total_chunks_count
|
|
8
|
+
|
|
9
|
+
@current_item = nil
|
|
10
|
+
@next_item = nil
|
|
11
|
+
@prev_item = nil
|
|
12
|
+
@counter = 0
|
|
13
|
+
@chunk_samples_counter = 0
|
|
14
|
+
@buffer = []
|
|
15
|
+
@intermediary_chunks_count = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def count_hidden(sample_number)
|
|
19
|
+
(@total_chunks_count - @counter).times do
|
|
20
|
+
@counter += 1
|
|
21
|
+
@chunk_samples_counter += @current_item[:samples_per_chunk]
|
|
22
|
+
|
|
23
|
+
return @counter if sample_number <= @chunk_samples_counter
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def search_in_buffer(sample_number)
|
|
30
|
+
@buffer.each do |samples_per_chunk|
|
|
31
|
+
@chunk_samples_counter += samples_per_chunk
|
|
32
|
+
@counter += 1
|
|
33
|
+
@buffer.shift
|
|
34
|
+
|
|
35
|
+
return @counter if sample_number <= @chunk_samples_counter
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find_chunk_number(sample_number)
|
|
42
|
+
return @counter if sample_number <= @chunk_samples_counter
|
|
43
|
+
|
|
44
|
+
return count_hidden(sample_number) if @stsc_table.empty?
|
|
45
|
+
|
|
46
|
+
return search_in_buffer(sample_number) unless @buffer.empty?
|
|
47
|
+
|
|
48
|
+
@current_item = @stsc_table.shift
|
|
49
|
+
@next_item = @stsc_table.first || { samples_per_chunk: @current_item[:samples_per_chunk], first_chunk: @total_chunks_count }
|
|
50
|
+
|
|
51
|
+
chunks_between = [@next_item[:first_chunk] - @current_item[:first_chunk], 1].max
|
|
52
|
+
@buffer = Array.new(chunks_between) { @current_item[:samples_per_chunk] }
|
|
53
|
+
|
|
54
|
+
value_in_buffer = search_in_buffer(sample_number)
|
|
55
|
+
|
|
56
|
+
return value_in_buffer unless value_in_buffer.nil?
|
|
57
|
+
|
|
58
|
+
raise "Chunk not found for sample #{sample_number}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Stbl
|
|
4
|
+
module Samples
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def call(track_id, cache, stsc, stco, stss, stts, ctts, stsz)
|
|
8
|
+
sample_sizes = expand_stsz(stsz)
|
|
9
|
+
track_timescale = cache.track(track_id).fetch('timescale').to_i
|
|
10
|
+
stsc_calculator = SampleToChunkCalculator.new(stsc.table, stco.offsets.size)
|
|
11
|
+
ctts_calculator = CompositionOffsetCalculator.new(ctts&.entries_list)
|
|
12
|
+
delta_calculator = DeltaCalculator.new(stts.time_to_sample)
|
|
13
|
+
|
|
14
|
+
delta_sum_counter = 0
|
|
15
|
+
sample_offset_counter = 0
|
|
16
|
+
prev_sample_size = 0
|
|
17
|
+
prev_chunk_index = 0
|
|
18
|
+
|
|
19
|
+
cache.transaction do
|
|
20
|
+
stsz.sample_count.times do |index|
|
|
21
|
+
sample_index = index.next
|
|
22
|
+
sample_delta = delta_calculator.find_delta(sample_index)
|
|
23
|
+
|
|
24
|
+
chunk_index = stsc_calculator.find_chunk_number(sample_index)
|
|
25
|
+
|
|
26
|
+
chunk_offset = stco.offsets.fetch(chunk_index - 1)
|
|
27
|
+
is_keyframe = stss&.sync_sample&.include?(sample_index) ? 'TRUE' : 'FALSE'
|
|
28
|
+
|
|
29
|
+
if prev_chunk_index.zero? || prev_chunk_index != chunk_index
|
|
30
|
+
sample_offset_counter = chunk_offset
|
|
31
|
+
else
|
|
32
|
+
sample_offset_counter += prev_sample_size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
prev_sample_size = sample_sizes[index].to_i
|
|
36
|
+
|
|
37
|
+
cache.add_sample(
|
|
38
|
+
track_id: track_id.to_i,
|
|
39
|
+
chunk_index: chunk_index.to_i,
|
|
40
|
+
sample_index: sample_index.to_i,
|
|
41
|
+
sample_size: sample_sizes[index].to_i,
|
|
42
|
+
sample_offset: sample_offset_counter,
|
|
43
|
+
chunk_offset: chunk_offset,
|
|
44
|
+
original_chunk_offset: chunk_offset,
|
|
45
|
+
sample_delta: sample_delta.to_i,
|
|
46
|
+
delta_sum: delta_sum_counter,
|
|
47
|
+
absolute_delta_sum: (delta_sum_counter.to_f / track_timescale * 1000.0).ceil,
|
|
48
|
+
composition_offset: ctts_calculator.find_composition_offset(sample_index),
|
|
49
|
+
is_keyframe: is_keyframe,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
delta_sum_counter += sample_delta
|
|
53
|
+
prev_chunk_index = chunk_index
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def expand_stsz(stsz)
|
|
59
|
+
return Hash.new(stsz.sample_size) if stsz.sample_size.to_i.nonzero?
|
|
60
|
+
|
|
61
|
+
stsz.sample_sizes
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch_composition_offset_by_sample_index(ctts, sample_number)
|
|
65
|
+
return nil if ctts.nil?
|
|
66
|
+
|
|
67
|
+
total_samples = 0
|
|
68
|
+
|
|
69
|
+
ctts.entries_list.each do |entry|
|
|
70
|
+
total_samples += entry.sample_count
|
|
71
|
+
|
|
72
|
+
return entry.fetch(:composition_offset) if total_samples >= sample_number
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise "Sample Composition offset not found for sample index: #{sample_index}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
module Memoize
|
|
3
|
+
module Track
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
using ::MP4::Ext::Refinements
|
|
7
|
+
|
|
8
|
+
def call(cache, track)
|
|
9
|
+
current_track_id = get_track_id(track)
|
|
10
|
+
::MP4.logger.debug { "track #{current_track_id}: memoize started" }
|
|
11
|
+
|
|
12
|
+
memoize_overall_track_information(current_track_id, cache, track)
|
|
13
|
+
memoize_elst(current_track_id, cache, track)
|
|
14
|
+
memoize_dinf(current_track_id, cache, track)
|
|
15
|
+
memoize_stbl(current_track_id, cache, track)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def memoize_overall_track_information(track_id, cache, track)
|
|
19
|
+
codec_data = Stbl::Codec.parse(track.dig!(:mdia, :minf, :stbl, :stsd, :content))
|
|
20
|
+
handler_type = track.dig!(:mdia, :hdlr, :handler_type).to_s
|
|
21
|
+
|
|
22
|
+
cache.add_track(
|
|
23
|
+
track_id: track_id,
|
|
24
|
+
handler_type: handler_type,
|
|
25
|
+
handler_name: track.dig!(:mdia, :hdlr, :handler_name).to_s,
|
|
26
|
+
have_composition_time: track.dig!(:mdia, :minf, :stbl).fetch(:ctts, nil) ? 1 : 0,
|
|
27
|
+
timescale: track.dig!(:mdia, :mdhd, :timescale).to_i,
|
|
28
|
+
creation_time: track.dig!(:mdia, :mdhd, :creation_time).to_i,
|
|
29
|
+
width: track.dig!(:tkhd, :width).to_i,
|
|
30
|
+
height: track.dig!(:tkhd, :height).to_i,
|
|
31
|
+
volume: track.dig!(:tkhd, :volume).to_i,
|
|
32
|
+
channel_count: codec_data.dig(0, :channel_count),
|
|
33
|
+
par: codec_data.dig(0, :par),
|
|
34
|
+
sar: codec_data.dig(0, :sar),
|
|
35
|
+
codec: codec_data.dig(0, :codec),
|
|
36
|
+
sample_rate: codec_data.dig(0, :sample_rate),
|
|
37
|
+
max_bitrate: codec_data.dig(0, :max_bitrate),
|
|
38
|
+
avg_bitrate: codec_data.dig(0, :avg_bitrate),
|
|
39
|
+
mime_type: { 'vide' => 'video/mp4', 'soun' => 'audio/mp4' }.fetch(handler_type),
|
|
40
|
+
alternate_group: track.dig!(:tkhd, :alternate_group),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
::MP4.logger.debug { "track #{track_id}: metadata memoized (#{handler_type}, #{codec_data.dig(0, :codec)})" }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def memoize_dinf(track_id, cache, track)
|
|
47
|
+
cache.add_dinf_content(
|
|
48
|
+
track_id: track_id,
|
|
49
|
+
content: track.dig!(:mdia, :minf, :dinf, :content),
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def memoize_stbl(current_track_id, cache, track)
|
|
54
|
+
stbl_start = Time.now
|
|
55
|
+
|
|
56
|
+
Stbl::BinaryStsd.call(current_track_id, cache, track.dig!(:mdia, :minf, :stbl, :stsd, :content))
|
|
57
|
+
|
|
58
|
+
Stbl::Chunks.call(
|
|
59
|
+
current_track_id, cache,
|
|
60
|
+
track.dig!(:mdia, :minf, :stbl, :stco),
|
|
61
|
+
track.dig!(:mdia, :minf, :stbl, :stsc),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
Stbl::Samples.call(
|
|
65
|
+
current_track_id, cache,
|
|
66
|
+
track.dig!(:mdia, :minf, :stbl).fetch(:stsc, nil),
|
|
67
|
+
track.dig!(:mdia, :minf, :stbl).fetch(:stco, nil),
|
|
68
|
+
track.dig!(:mdia, :minf, :stbl).fetch(:stss, nil),
|
|
69
|
+
track.dig!(:mdia, :minf, :stbl).fetch(:stts, nil),
|
|
70
|
+
track.dig!(:mdia, :minf, :stbl).fetch(:ctts, nil),
|
|
71
|
+
track.dig!(:mdia, :minf, :stbl).fetch(:stsz, nil),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
::MP4.logger.debug { "track #{current_track_id}: stbl memoized in #{(Time.now - stbl_start).round(2)} s" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def memoize_elst(current_track_id, cache, track)
|
|
78
|
+
elst_matrix = track.dig(:edts, :elst, :matrix)
|
|
79
|
+
|
|
80
|
+
return if elst_matrix.nil?
|
|
81
|
+
|
|
82
|
+
elst_matrix.each do |matrix|
|
|
83
|
+
cache.add_elst_list_item(
|
|
84
|
+
track_id: current_track_id,
|
|
85
|
+
track_duration: matrix.fetch(:segment_duration),
|
|
86
|
+
media_time: matrix.fetch(:media_time),
|
|
87
|
+
media_rate: matrix.fetch(:media_rate),
|
|
88
|
+
media_rate_fraction: matrix.fetch(:media_rate_fraction),
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get_track_id(track)
|
|
94
|
+
track.fetch(:tkhd).fetch(:track_id).to_i
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module MP4::Metadata
|
|
2
|
+
class Service
|
|
3
|
+
def self.call(cache:, parsed_mp4:)
|
|
4
|
+
new(cache).memoize!(parsed_mp4)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def initialize(cache)
|
|
8
|
+
@cache = cache
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def memoize!(parsed_mp4)
|
|
12
|
+
started_at = Time.now
|
|
13
|
+
::MP4.logger.info { 'memoize: starting' }
|
|
14
|
+
|
|
15
|
+
memoize_ftyp(parsed_mp4)
|
|
16
|
+
memoize_tracks(parsed_mp4)
|
|
17
|
+
|
|
18
|
+
::MP4.logger.info { "memoize: done in #{(Time.now - started_at).round(2)} s" }
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def memoize_ftyp(parsed_mp4)
|
|
23
|
+
Memoize::Ftyp.call(@cache, parsed_mp4.ftyp)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def memoize_tracks(parsed_mp4)
|
|
27
|
+
Memoize::Tracks.call(@cache, parsed_mp4.moov)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|