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,36 @@
|
|
|
1
|
+
module MP4::Nolan
|
|
2
|
+
module Service::Sound
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def compute(cache, segment_index, track:, from_timestamp:, to_timestamp:, sample_timescale:)
|
|
6
|
+
track_id = track.fetch('track_id')
|
|
7
|
+
relative_from = from_timestamp || 0
|
|
8
|
+
|
|
9
|
+
min_chunk_offset = 0
|
|
10
|
+
current_chunk_index = 0
|
|
11
|
+
|
|
12
|
+
cache.samples_between_ms(track_id, relative_from, to_timestamp).each_with_index do |original_sample, sample_index_incr|
|
|
13
|
+
if current_chunk_index != original_sample.fetch('chunk_index')
|
|
14
|
+
current_chunk_index = original_sample.fetch('chunk_index')
|
|
15
|
+
min_chunk_offset = original_sample.fetch('offset')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
cache.add_segment_sample(
|
|
19
|
+
track_id: track_id,
|
|
20
|
+
segment_index: segment_index,
|
|
21
|
+
chunk_index: original_sample.fetch('chunk_index'),
|
|
22
|
+
sample_index: sample_index_incr.succ,
|
|
23
|
+
sample_size: original_sample.fetch('size'),
|
|
24
|
+
sample_offset: original_sample.fetch('offset'),
|
|
25
|
+
chunk_offset: min_chunk_offset,
|
|
26
|
+
original_chunk_offset: min_chunk_offset,
|
|
27
|
+
sample_delta: original_sample.fetch('delta'),
|
|
28
|
+
delta_sum: original_sample.fetch('delta_sum'),
|
|
29
|
+
absolute_delta_sum: 0,
|
|
30
|
+
composition_offset: original_sample.fetch('c_delta'),
|
|
31
|
+
is_keyframe: original_sample.fetch('keyframe'),
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module MP4::Nolan
|
|
2
|
+
module Service::Video
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def compute(cache, segment_index, track:, from_timestamp:, to_timestamp:, sample_timescale:)
|
|
6
|
+
track_id = track.fetch('track_id')
|
|
7
|
+
|
|
8
|
+
min_chunk_offset = 0
|
|
9
|
+
current_chunk_index = 0
|
|
10
|
+
|
|
11
|
+
cache.samples_between_ms(track_id, from_timestamp, to_timestamp).each_with_index do |original_sample, sample_index_incr|
|
|
12
|
+
if current_chunk_index != original_sample.fetch('chunk_index')
|
|
13
|
+
current_chunk_index = original_sample.fetch('chunk_index')
|
|
14
|
+
min_chunk_offset = original_sample.fetch('offset')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
cache.add_segment_sample(
|
|
18
|
+
track_id: track_id,
|
|
19
|
+
segment_index: segment_index,
|
|
20
|
+
chunk_index: original_sample.fetch('chunk_index'),
|
|
21
|
+
sample_index: sample_index_incr.succ,
|
|
22
|
+
sample_size: original_sample.fetch('size'),
|
|
23
|
+
sample_offset: original_sample.fetch('offset'),
|
|
24
|
+
chunk_offset: min_chunk_offset,
|
|
25
|
+
original_chunk_offset: min_chunk_offset,
|
|
26
|
+
sample_delta: original_sample.fetch('delta'),
|
|
27
|
+
delta_sum: original_sample.fetch('delta_sum'),
|
|
28
|
+
absolute_delta_sum: 0,
|
|
29
|
+
composition_offset: original_sample.fetch('c_delta'),
|
|
30
|
+
is_keyframe: original_sample.fetch('keyframe'),
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module MP4::Nolan
|
|
2
|
+
module Service
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def divide_into_segments(cache)
|
|
6
|
+
segments = compute_segments(cache)
|
|
7
|
+
|
|
8
|
+
segments.each_with_index do |segment_args, segment_index|
|
|
9
|
+
compile_segment(cache, segment_index, *segment_args)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def compute_segments(cache)
|
|
16
|
+
track_id = cache.first_video_track_id
|
|
17
|
+
return [] if track_id.nil?
|
|
18
|
+
|
|
19
|
+
video_track = cache.track(track_id)
|
|
20
|
+
timescale = video_track.fetch('timescale')
|
|
21
|
+
|
|
22
|
+
divide_video_by_keyframes(cache, track_id, timescale)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def compile_segment(cache, segment_index, _from_keyframe, _to_keyframe, segment_start_timestamp, segment_end_timestamp, _duration, previous_chunk_end_delta_sum, sample_timescale)
|
|
26
|
+
cache.tracks.each do |track|
|
|
27
|
+
case track['handler_type']
|
|
28
|
+
when 'vide'
|
|
29
|
+
Video.compute(cache, segment_index,
|
|
30
|
+
track: track,
|
|
31
|
+
from_timestamp: segment_start_timestamp,
|
|
32
|
+
to_timestamp: segment_end_timestamp,
|
|
33
|
+
sample_timescale: sample_timescale,)
|
|
34
|
+
when 'soun'
|
|
35
|
+
Sound.compute(cache, segment_index,
|
|
36
|
+
track: track,
|
|
37
|
+
from_timestamp: previous_chunk_end_delta_sum,
|
|
38
|
+
to_timestamp: segment_end_timestamp,
|
|
39
|
+
sample_timescale: sample_timescale,)
|
|
40
|
+
else
|
|
41
|
+
MP4.logger.warn "Nolan::Service: skipping unsupported track #{track['handler_type']}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def divide_video_by_keyframes(cache, track_id, timescale)
|
|
47
|
+
first_keyframe = cache.first_sample(track_id)
|
|
48
|
+
latest_keyframe = cache.last_sample(track_id)
|
|
49
|
+
return [] if first_keyframe.nil? || latest_keyframe.nil?
|
|
50
|
+
|
|
51
|
+
original_keyframes = cache.keyframes_grouped_by_second(track_id)
|
|
52
|
+
return [] if original_keyframes.empty?
|
|
53
|
+
|
|
54
|
+
chunks = []
|
|
55
|
+
last_index = original_keyframes.size - 1
|
|
56
|
+
|
|
57
|
+
original_keyframes.each_with_index do |keyframe, idx|
|
|
58
|
+
keyframes =
|
|
59
|
+
if idx == last_index
|
|
60
|
+
[keyframe.fetch('sample_index'), latest_keyframe.fetch('sample_index')]
|
|
61
|
+
elsif idx.zero? && keyframe.fetch('delta_sum').to_i.nonzero?
|
|
62
|
+
[first_keyframe.fetch('sample_index'), keyframe.fetch('sample_index')]
|
|
63
|
+
else
|
|
64
|
+
[keyframe.fetch('sample_index'), original_keyframes[idx + 1].fetch('sample_index') - 1]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
previous_chunk_end_delta_sum = chunks.dig(-1, 3)
|
|
68
|
+
duration = cache.sum_delta_between(track_id, keyframes.first, keyframes.last).to_f
|
|
69
|
+
|
|
70
|
+
chunks << [
|
|
71
|
+
keyframes.fetch(0),
|
|
72
|
+
keyframes.fetch(1),
|
|
73
|
+
cache.sample_delta_sum_ms(track_id, keyframes.first).to_i,
|
|
74
|
+
cache.sample_delta_sum_ms(track_id, keyframes.last).to_i,
|
|
75
|
+
(duration / timescale.to_f).round(3),
|
|
76
|
+
previous_chunk_end_delta_sum,
|
|
77
|
+
timescale,
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
chunks
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/mp4/nolan.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module MP4::Parser
|
|
2
|
+
module MainBoxes
|
|
3
|
+
class BoxAlreadyDefinedError < StandardError; end
|
|
4
|
+
class UnnamedBoxFoundError < StandardError; end
|
|
5
|
+
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse(source, _offset = 0, _nested = [])
|
|
9
|
+
struct = {}
|
|
10
|
+
|
|
11
|
+
10.times do
|
|
12
|
+
break if source.eof?
|
|
13
|
+
|
|
14
|
+
current_offset = source.pos
|
|
15
|
+
header = get_header(source)
|
|
16
|
+
box_name = header[:box_name]
|
|
17
|
+
box_size = header[:box_size]
|
|
18
|
+
|
|
19
|
+
raise BoxAlreadyDefinedError.new(message: "Box #{box_name} already defined") if struct.key?(box_name)
|
|
20
|
+
raise UnnamedBoxFoundError.new(message: "Box #{box_name} already defined") if box_name.empty?
|
|
21
|
+
|
|
22
|
+
struct[box_name.to_sym] = [current_offset, box_size]
|
|
23
|
+
|
|
24
|
+
source.set_seek(current_offset + box_size)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
struct
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_header(source)
|
|
31
|
+
bytes = source.read(8)
|
|
32
|
+
box_size = BinData::Uint32be.read(bytes[0..4])
|
|
33
|
+
box_name = bytes[4..8]
|
|
34
|
+
::MP4.logger.debug { "top-level box: #{box_name}/#{box_size}" } if ::MP4.verbose?
|
|
35
|
+
if box_size == 1
|
|
36
|
+
extra_size_bytes = source.read(8)
|
|
37
|
+
box_size = BinData::Uint64be.read(extra_size_bytes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
{ box_size: box_size, box_name: box_name }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module MP4::Parser
|
|
2
|
+
class Moov
|
|
3
|
+
attr_reader :bytes, :mvhd, :tracks
|
|
4
|
+
|
|
5
|
+
def self.parse(bytes)
|
|
6
|
+
metadata = new(bytes)
|
|
7
|
+
metadata.parse
|
|
8
|
+
metadata
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(bytes)
|
|
12
|
+
@bytes = bytes
|
|
13
|
+
@tracks = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse
|
|
17
|
+
_header = bytes.read(8)
|
|
18
|
+
|
|
19
|
+
until bytes.eof?
|
|
20
|
+
box_size = BinData::Uint32be.read(bytes.read(4))
|
|
21
|
+
box_type = bytes.read(4)
|
|
22
|
+
box_bytes = StringIO.new(bytes.read(box_size - MP4::Constants::GENERIC_HEADER_SIZE))
|
|
23
|
+
|
|
24
|
+
send("parse_#{box_type}", box_bytes)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_mvhd(bytes)
|
|
29
|
+
@mvhd = MP4::Binary::Mvhd.read(bytes).snapshot
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse_trak(bytes)
|
|
33
|
+
@tracks << MP4::Parser::Track.parse(bytes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_iods(_bytes); end
|
|
37
|
+
|
|
38
|
+
def parse_udta(_bytes); end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module MP4::Parser
|
|
2
|
+
class Service
|
|
3
|
+
attr_reader :source, :ftyp, :moov, :mdat
|
|
4
|
+
|
|
5
|
+
def initialize(source:)
|
|
6
|
+
@source = source
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
parse
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse
|
|
14
|
+
main_boxes = MainBoxes.parse(source)
|
|
15
|
+
parse_ftyp(*main_boxes.fetch(:ftyp))
|
|
16
|
+
parse_moov(*main_boxes.fetch(:moov))
|
|
17
|
+
parse_mdat(*main_boxes.fetch(:mdat))
|
|
18
|
+
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse_ftyp(offset, box_size)
|
|
23
|
+
source.set_seek(offset + MP4::Constants::GENERIC_HEADER_SIZE)
|
|
24
|
+
|
|
25
|
+
@ftyp = MP4::Binary::Ftyp.read(
|
|
26
|
+
StringIO.new(source.read(box_size - MP4::Constants::GENERIC_HEADER_SIZE)),
|
|
27
|
+
).snapshot
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_moov(offset, box_size)
|
|
31
|
+
source.set_seek(offset)
|
|
32
|
+
|
|
33
|
+
@moov = Moov.parse(
|
|
34
|
+
StringIO.new(source.read(box_size)),
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_mdat(offset, _box_size)
|
|
39
|
+
@mdat = { offset: offset }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# require_relative './lib/constants'
|
|
45
|
+
# require_relative './lib/main_boxes'
|
|
46
|
+
# require_relative './lib/moov'
|
|
47
|
+
# require_relative './lib/recursive_parser'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module MP4::Parser
|
|
2
|
+
class Track
|
|
3
|
+
attr_reader :bytes
|
|
4
|
+
|
|
5
|
+
def self.parse(bytes)
|
|
6
|
+
new(bytes).parse
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(bytes)
|
|
10
|
+
@bytes = bytes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse
|
|
14
|
+
parse_recursive
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def parse_recursive(nested_bytes = nil, list = [])
|
|
20
|
+
structure = []
|
|
21
|
+
nested_bytes ||= bytes
|
|
22
|
+
|
|
23
|
+
loop do
|
|
24
|
+
box_bytes = nested_bytes
|
|
25
|
+
break if box_bytes.eof? || box_bytes.nil?
|
|
26
|
+
|
|
27
|
+
box_offset = box_bytes.pos
|
|
28
|
+
box_size = BinData::Uint32be.read(box_bytes.read(4))
|
|
29
|
+
box_type = box_bytes.read(4)
|
|
30
|
+
|
|
31
|
+
box_bytes = StringIO.new(box_bytes.read(box_size - MP4::Constants::GENERIC_HEADER_SIZE))
|
|
32
|
+
|
|
33
|
+
structure << [box_type, box_offset, box_size, box_bytes]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
structure.each_with_object({}) do |(box_type, _box_offset, _box_size, box_bytes), buffer|
|
|
37
|
+
::MP4.logger.debug { "parsing atom: #{list.join('->')} #{box_type}" } if ::MP4.verbose?
|
|
38
|
+
|
|
39
|
+
buffer[box_type.to_sym] = if MP4::Constants::PARSERS.key?(box_type)
|
|
40
|
+
parse_atom(box_type, box_bytes)
|
|
41
|
+
else
|
|
42
|
+
parse_recursive(box_bytes, [*list, box_type])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_atom(box_type, box_bytes)
|
|
48
|
+
MP4::Constants::PARSERS.fetch(box_type)
|
|
49
|
+
|
|
50
|
+
serializer_klass = Kernel.const_get(MP4::Constants::PARSERS.fetch(box_type))
|
|
51
|
+
|
|
52
|
+
serializer_klass.read(box_bytes).snapshot
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/mp4/rb.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module MP4::Source
|
|
2
|
+
module Base
|
|
3
|
+
REQUIRED_METHODS = %i[read set_seek reset_seek open! eof? pos uri].freeze
|
|
4
|
+
|
|
5
|
+
def self.included(klass)
|
|
6
|
+
klass.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def from_uri(_uri)
|
|
11
|
+
raise NotImplementedError, "#{name}.from_uri must be implemented"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(from, to)
|
|
16
|
+
set_seek(from)
|
|
17
|
+
read(to - from + 1)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module MP4::Source
|
|
2
|
+
class Local
|
|
3
|
+
include Base
|
|
4
|
+
|
|
5
|
+
attr_reader :path_to_file
|
|
6
|
+
|
|
7
|
+
def self.from_uri(uri, base_dir: nil)
|
|
8
|
+
raise ArgumentError, "expected local: URI, got #{uri.inspect}" unless uri.start_with?('local:')
|
|
9
|
+
|
|
10
|
+
path = uri.sub(/\Alocal:/, '')
|
|
11
|
+
resolved = resolve_path(path, base_dir)
|
|
12
|
+
new(resolved.to_s)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.resolve_path(path, base_dir)
|
|
16
|
+
candidate = Pathname.new(path)
|
|
17
|
+
return candidate if base_dir.nil?
|
|
18
|
+
|
|
19
|
+
root = Pathname.new(base_dir).expand_path
|
|
20
|
+
absolute = (candidate.absolute? ? candidate : root.join(candidate)).expand_path
|
|
21
|
+
|
|
22
|
+
unless absolute.to_s.start_with?(root.to_s + File::SEPARATOR) || absolute.to_s == root.to_s
|
|
23
|
+
raise ArgumentError, "path escapes base_dir: #{path.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
absolute
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(path_to_file)
|
|
30
|
+
@path_to_file = path_to_file.to_s
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def uri
|
|
34
|
+
"local:#{@path_to_file}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read(n)
|
|
38
|
+
file.read(n)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_seek(n)
|
|
42
|
+
file.seek(n)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reset_seek
|
|
46
|
+
file.seek(0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def open!
|
|
50
|
+
file
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def eof?
|
|
55
|
+
file.eof?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pos
|
|
59
|
+
file.pos
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def mtime
|
|
63
|
+
File.mtime(@path_to_file)
|
|
64
|
+
rescue Errno::ENOENT
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def close
|
|
69
|
+
@file&.close
|
|
70
|
+
@file = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def file
|
|
76
|
+
@file ||= File.open(@path_to_file, 'rb')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module MP4::Source
|
|
2
|
+
class S3
|
|
3
|
+
include Base
|
|
4
|
+
|
|
5
|
+
attr_reader :bucket, :key
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def from_uri(uri, client: nil)
|
|
9
|
+
match = uri.match(%r{\As3://(?<bucket>[^/]+)/(?<key>.+)\z})
|
|
10
|
+
raise ArgumentError, "expected s3://BUCKET/KEY URI, got #{uri.inspect}" unless match
|
|
11
|
+
|
|
12
|
+
new(bucket: match[:bucket], key: match[:key], client: client)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def shared_client
|
|
16
|
+
@shared_client_mutex ||= Mutex.new
|
|
17
|
+
@shared_client_mutex.synchronize do
|
|
18
|
+
@shared_client ||= build_client
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_shared_client!
|
|
23
|
+
@shared_client_mutex&.synchronize { @shared_client = nil }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_client
|
|
29
|
+
require 'aws-sdk-s3'
|
|
30
|
+
options = {
|
|
31
|
+
region: ENV.fetch('AWS_REGION', 'us-east-1'),
|
|
32
|
+
http_open_timeout: Integer(ENV.fetch('S3_HTTP_OPEN_TIMEOUT', '5')),
|
|
33
|
+
http_read_timeout: Integer(ENV.fetch('S3_HTTP_READ_TIMEOUT', '30')),
|
|
34
|
+
http_idle_timeout: Integer(ENV.fetch('S3_HTTP_IDLE_TIMEOUT', '30')),
|
|
35
|
+
}
|
|
36
|
+
endpoint = ENV['MINIO_ENDPOINT'] || ENV['S3_ENDPOINT']
|
|
37
|
+
if endpoint
|
|
38
|
+
options[:endpoint] = endpoint
|
|
39
|
+
options[:force_path_style] = true
|
|
40
|
+
end
|
|
41
|
+
access = ENV['MINIO_ACCESS_KEY_ID'] || ENV['AWS_ACCESS_KEY_ID']
|
|
42
|
+
secret = ENV['MINIO_SECRET_ACCESS_KEY'] || ENV['AWS_SECRET_ACCESS_KEY']
|
|
43
|
+
if access && secret
|
|
44
|
+
options[:access_key_id] = access
|
|
45
|
+
options[:secret_access_key] = secret
|
|
46
|
+
end
|
|
47
|
+
Aws::S3::Client.new(**options)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(bucket:, key:, client: nil)
|
|
52
|
+
@bucket = bucket
|
|
53
|
+
@key = key
|
|
54
|
+
@client = client
|
|
55
|
+
@position = 0
|
|
56
|
+
@file_size = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def uri
|
|
60
|
+
"s3://#{@bucket}/#{@key}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def open!
|
|
64
|
+
size
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def read(n)
|
|
69
|
+
return ''.b if eof?
|
|
70
|
+
|
|
71
|
+
to = [@position + n - 1, size - 1].min
|
|
72
|
+
data = get_range(@position, to)
|
|
73
|
+
@position += data.bytesize
|
|
74
|
+
data
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fetch(from, to)
|
|
78
|
+
get_range(from, to)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def set_seek(n)
|
|
82
|
+
@position = n.clamp(0, size)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def reset_seek
|
|
86
|
+
@position = 0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def eof?
|
|
90
|
+
@position >= size
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def pos
|
|
94
|
+
@position
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def size
|
|
98
|
+
@file_size ||= client.head_object(bucket: @bucket, key: @key).content_length.to_i
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def close
|
|
102
|
+
# No per-instance connection to close; connections live in the shared
|
|
103
|
+
# client's connection pool and are reused across sources.
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def client
|
|
110
|
+
@client ||= self.class.shared_client
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def get_range(from, to)
|
|
114
|
+
resp = client.get_object(bucket: @bucket, key: @key, range: "bytes=#{from}-#{to}")
|
|
115
|
+
resp.body.read.b
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|