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,25 @@
|
|
|
1
|
+
module M4S::Binary
|
|
2
|
+
class DefaultSampleFlags < BinData::Record
|
|
3
|
+
endian :big
|
|
4
|
+
|
|
5
|
+
bit2 :is_leading
|
|
6
|
+
bit2 :sample_depends_on
|
|
7
|
+
bit2 :sample_is_depended_on
|
|
8
|
+
bit2 :sample_has_redundancy
|
|
9
|
+
bit3 :sample_padding_value
|
|
10
|
+
bit1 :sample_is_sync
|
|
11
|
+
bit16 :sample_degradation_priority
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Trex < BinData::Record
|
|
15
|
+
endian :big
|
|
16
|
+
|
|
17
|
+
uint8 :version
|
|
18
|
+
bit24 :flags
|
|
19
|
+
uint32 :track_id
|
|
20
|
+
uint32 :default_sample_description_index
|
|
21
|
+
uint32 :default_sample_duration
|
|
22
|
+
uint32 :default_sample_size
|
|
23
|
+
default_sample_flags :default_sample_flags
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module M4S
|
|
2
|
+
module Binary
|
|
3
|
+
class Trun < BinData::Record
|
|
4
|
+
endian :big
|
|
5
|
+
|
|
6
|
+
FLAGS = {
|
|
7
|
+
data_offset: 0x000001,
|
|
8
|
+
first_sample_flags: 0x000004,
|
|
9
|
+
sample_duration: 0x000100,
|
|
10
|
+
sample_size: 0x000200,
|
|
11
|
+
sample_flags: 0x000400,
|
|
12
|
+
sample_composition_time_offset: 0x000800,
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
uint8 :version
|
|
16
|
+
bit24 :flags
|
|
17
|
+
uint32 :sample_count
|
|
18
|
+
|
|
19
|
+
int32 :data_offset, onlyif: -> { flags & FLAGS[:data_offset] != 0 }
|
|
20
|
+
uint32 :first_sample_flags, onlyif: -> { flags & FLAGS[:first_sample_flags] != 0 }
|
|
21
|
+
|
|
22
|
+
array :samples, initial_length: :sample_count do
|
|
23
|
+
uint32 :sample_duration, onlyif: -> { parent.flags & FLAGS[:sample_duration] != 0 }
|
|
24
|
+
uint32 :sample_size, onlyif: -> { parent.flags & FLAGS[:sample_size] != 0 }
|
|
25
|
+
uint32 :sample_flags, onlyif: -> { parent.flags & FLAGS[:sample_flags] != 0 }
|
|
26
|
+
int32 :sample_composition_time_offset, onlyif: -> { parent.flags & FLAGS[:sample_composition_time_offset] != 0 }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module M4S::Dash
|
|
2
|
+
module Manifest
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
VIDEO_TRACK = 'video/mp4'.freeze
|
|
6
|
+
AUDIO_TRACK = 'audio/mp4'.freeze
|
|
7
|
+
TRACKS_PRIORITY = { VIDEO_TRACK => 0, AUDIO_TRACK => 1 }.freeze
|
|
8
|
+
SCHEMA_LOCATION = 'urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd'.freeze
|
|
9
|
+
PROFILE = 'urn:mpeg:dash:profile:isoff-live:2011'.freeze
|
|
10
|
+
|
|
11
|
+
def generate(mp4, init_segment_path:, segment_path:)
|
|
12
|
+
media_presentation_duration = calculate_media_presentation_duration(mp4)
|
|
13
|
+
max_segment_duration = calculate_max_segment_duration(mp4)
|
|
14
|
+
|
|
15
|
+
init_template = join_path(init_segment_path, 'init-$RepresentationID$.mp4')
|
|
16
|
+
media_template = join_path(segment_path, 'chunk-$RepresentationID$-$Number%05d$.m4s')
|
|
17
|
+
|
|
18
|
+
builder = Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml|
|
|
19
|
+
xml.MPD(
|
|
20
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
|
|
21
|
+
'xmlns' => 'urn:mpeg:dash:schema:mpd:2011',
|
|
22
|
+
'xmlns:xlink' => 'http://www.w3.org/1999/xlink',
|
|
23
|
+
'xsi:schemaLocation' => SCHEMA_LOCATION,
|
|
24
|
+
'profiles' => PROFILE,
|
|
25
|
+
'type' => 'static',
|
|
26
|
+
'mediaPresentationDuration' => format_duration(media_presentation_duration),
|
|
27
|
+
'maxSegmentDuration' => format_duration(max_segment_duration),
|
|
28
|
+
'minBufferTime' => format_duration(max_segment_duration * 2.0),
|
|
29
|
+
) do
|
|
30
|
+
xml.ProgramInformation
|
|
31
|
+
xml.ServiceDescription(id: '0')
|
|
32
|
+
xml.Period(id: '0', start: 'PT0.0S') do
|
|
33
|
+
sorted_tracks(mp4).each_with_index do |track, index|
|
|
34
|
+
render_adaptation_set(xml, mp4, track, index, init_template, media_template)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
builder.to_xml
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def calculate_max_segment_duration(mp4)
|
|
44
|
+
track = primary_track(mp4)
|
|
45
|
+
max = mp4.max_segment_duration_ticks(track.fetch('track_id')).to_f
|
|
46
|
+
max / track.fetch('timescale').to_f
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def calculate_media_presentation_duration(mp4)
|
|
50
|
+
track = primary_track(mp4)
|
|
51
|
+
mp4.track_sample_delta_sum(track.fetch('track_id')).to_f / track.fetch('timescale')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_duration(float_seconds)
|
|
55
|
+
total_seconds = float_seconds.to_f
|
|
56
|
+
minutes = (total_seconds / 60).to_i
|
|
57
|
+
seconds = total_seconds % 60
|
|
58
|
+
|
|
59
|
+
if minutes.positive?
|
|
60
|
+
seconds.positive? ? "PT#{minutes}M#{seconds.round(1)}S" : "PT#{minutes}M"
|
|
61
|
+
else
|
|
62
|
+
"PT#{seconds.round(1)}S"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def sorted_tracks(mp4)
|
|
69
|
+
mp4.tracks.sort_by { |t| TRACKS_PRIORITY.fetch(t['mime_type'], TRACKS_PRIORITY.size) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def primary_track(mp4)
|
|
73
|
+
mp4.tracks.find { |t| t.fetch('handler_type') == 'vide' } ||
|
|
74
|
+
mp4.tracks.find { |t| t.fetch('handler_type') == 'soun' }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_adaptation_set(xml, mp4, track, index, init_template, media_template)
|
|
78
|
+
xml.AdaptationSet(adaptation_set_options(track, index)) do
|
|
79
|
+
xml.Representation(representation_options(track, index)) do
|
|
80
|
+
if track.fetch('mime_type') == AUDIO_TRACK
|
|
81
|
+
xml.AudioChannelConfiguration(
|
|
82
|
+
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
|
|
83
|
+
value: track.fetch('channel_count') || 2,
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
xml.SegmentTemplate(
|
|
88
|
+
timescale: track.fetch('timescale'),
|
|
89
|
+
initialization: init_template,
|
|
90
|
+
media: media_template,
|
|
91
|
+
startNumber: '1',
|
|
92
|
+
) do
|
|
93
|
+
xml.SegmentTimeline do
|
|
94
|
+
mp4.segment_duration_ticks_by_index(track.fetch('track_id')).each_with_index do |segment, seg_i|
|
|
95
|
+
if seg_i.zero?
|
|
96
|
+
xml.S(t: '0', d: segment.fetch('duration'))
|
|
97
|
+
else
|
|
98
|
+
xml.S(d: segment.fetch('duration'))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def adaptation_set_options(track, index)
|
|
108
|
+
opts = { id: index.to_s, startWithSAP: '1', segmentAlignment: 'true', bitstreamSwitching: 'true' }
|
|
109
|
+
if track.fetch('mime_type') == VIDEO_TRACK
|
|
110
|
+
opts.merge!(
|
|
111
|
+
contentType: 'video',
|
|
112
|
+
maxWidth: track.fetch('width').to_s,
|
|
113
|
+
maxHeight: track.fetch('height').to_s,
|
|
114
|
+
par: track.fetch('par'),
|
|
115
|
+
)
|
|
116
|
+
else
|
|
117
|
+
opts.merge!(contentType: 'audio')
|
|
118
|
+
end
|
|
119
|
+
opts
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def representation_options(track, index)
|
|
123
|
+
opts = {
|
|
124
|
+
id: index.to_s,
|
|
125
|
+
mimeType: track.fetch('mime_type'),
|
|
126
|
+
codecs: track.fetch('codec'),
|
|
127
|
+
bandwidth: bandwidth(track),
|
|
128
|
+
}
|
|
129
|
+
if track.fetch('mime_type') == VIDEO_TRACK
|
|
130
|
+
opts.merge!(width: track.fetch('width'), height: track.fetch('height'), sar: track.fetch('sar'))
|
|
131
|
+
elsif track.fetch('mime_type') == AUDIO_TRACK
|
|
132
|
+
opts.merge!(audioSamplingRate: track.fetch('sample_rate'))
|
|
133
|
+
end
|
|
134
|
+
opts
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def bandwidth(track)
|
|
138
|
+
value = track.fetch('avg_bitrate').to_i
|
|
139
|
+
value = track.fetch('max_bitrate').to_i if value.zero?
|
|
140
|
+
value.positive? ? value : 128_000
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def join_path(prefix, tail)
|
|
144
|
+
return tail if prefix.to_s.empty?
|
|
145
|
+
|
|
146
|
+
[prefix, tail].join('/')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Atom
|
|
3
|
+
attr_accessor :cache, :track, :segment_index, :track_id
|
|
4
|
+
|
|
5
|
+
def self.generate(cache, opts = {})
|
|
6
|
+
new(cache, opts).generate
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(cache, opts = {})
|
|
10
|
+
@cache = cache
|
|
11
|
+
@track = opts.fetch(:track, nil)
|
|
12
|
+
@track_id = track.fetch('track_id') if track
|
|
13
|
+
@segment_index = opts.fetch(:segment_index, nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate
|
|
17
|
+
[
|
|
18
|
+
*header,
|
|
19
|
+
binary_content,
|
|
20
|
+
].join
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def header
|
|
26
|
+
[
|
|
27
|
+
BinData::Uint32be.new(size).to_binary_s,
|
|
28
|
+
BinData::String.new(name).to_binary_s,
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def name
|
|
33
|
+
raise NotImplementedError.new(message: "#{__callee__} not implemented")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def size
|
|
37
|
+
::MP4::Constants::GENERIC_HEADER_SIZE + binary_content.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def binary_content
|
|
41
|
+
raise NotImplementedError.new(message: "#{__callee__} not implemented")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
module Builder
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def call(cache, source, track_id, segment_index)
|
|
6
|
+
track = cache.track(track_id)
|
|
7
|
+
|
|
8
|
+
container = Container.new(root: true)
|
|
9
|
+
|
|
10
|
+
container.add_atom Styp.generate(cache)
|
|
11
|
+
container.add_atom Sidx.generate(cache, track: track, segment_index: segment_index)
|
|
12
|
+
|
|
13
|
+
container.size
|
|
14
|
+
container.add_container :moof do |moof|
|
|
15
|
+
moof.add_atom Mfhd.generate(cache, segment_index: segment_index)
|
|
16
|
+
|
|
17
|
+
moof.add_container :traf do |traf|
|
|
18
|
+
traf.add_atom Tfhd.generate(cache, track: track, segment_index: segment_index)
|
|
19
|
+
traf.add_atom Tfdt.generate(cache, track: track, segment_index: segment_index)
|
|
20
|
+
trun_boilerplate = Trun.generate(cache, track: track, segment_index: segment_index)
|
|
21
|
+
|
|
22
|
+
data_offset = moof.size + traf.size + trun_boilerplate.size + ::MP4::Constants::GENERIC_HEADER_SIZE
|
|
23
|
+
traf.add_atom Trun.generate(cache, track: track, segment_index: segment_index, data_offset: data_offset)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
container.add_atom Mdat.generate(cache, source: source, track: track, segment_index: segment_index)
|
|
28
|
+
|
|
29
|
+
container
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Container
|
|
3
|
+
# include ::Layout::Base
|
|
4
|
+
|
|
5
|
+
attr_accessor :root, :name, :descendants
|
|
6
|
+
|
|
7
|
+
def initialize(root: false, name: nil)
|
|
8
|
+
@root = root
|
|
9
|
+
@name = name
|
|
10
|
+
@descendants ||= []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate
|
|
14
|
+
return binary_descendants if root
|
|
15
|
+
|
|
16
|
+
[
|
|
17
|
+
BinData::Uint32be.new(size).to_binary_s,
|
|
18
|
+
BinData::String.new(name).to_binary_s,
|
|
19
|
+
binary_descendants,
|
|
20
|
+
].join
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def binary_descendants
|
|
24
|
+
descendants.join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def size
|
|
28
|
+
binary_descendants.size + (root ? 0 : ::MP4::Constants::GENERIC_HEADER_SIZE)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_container(name, &block)
|
|
32
|
+
container = self.class.new(name: name)
|
|
33
|
+
|
|
34
|
+
block.call(container)
|
|
35
|
+
|
|
36
|
+
@descendants << container.generate
|
|
37
|
+
|
|
38
|
+
container
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_atom(atom)
|
|
42
|
+
@descendants << atom
|
|
43
|
+
atom
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_mdat(mp4, source)
|
|
47
|
+
@descendants << ::Layout::Mdat.new(mp4: mp4, source: source)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pack
|
|
51
|
+
return descendants.map(&:pack).join if root
|
|
52
|
+
|
|
53
|
+
[
|
|
54
|
+
*header,
|
|
55
|
+
descendants.map(&:pack),
|
|
56
|
+
].join
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module M4S
|
|
2
|
+
module Layout
|
|
3
|
+
class Ctts < Atom
|
|
4
|
+
using ::MP4::Ext::Refinements
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
'ctts'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def binary_content
|
|
13
|
+
@binary_content ||= ::MP4::Binary::Ctts.new(
|
|
14
|
+
entry_count: 0,
|
|
15
|
+
entries_list: [],
|
|
16
|
+
).to_binary_s
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module M4S
|
|
2
|
+
module Layout
|
|
3
|
+
class Dinf < Atom
|
|
4
|
+
using ::MP4::Ext::Refinements
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
'dinf'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def binary_content
|
|
13
|
+
@binary_content ||= ::MP4::Binary::Dinf.new(content: cache.dinf_content(track_id)).to_binary_s
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module M4S
|
|
2
|
+
module Layout
|
|
3
|
+
class Elst < Atom
|
|
4
|
+
using ::MP4::Ext::Refinements
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
'elst'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def binary_content
|
|
13
|
+
@binary_content ||= ::MP4::Binary::Elst.new(
|
|
14
|
+
version: 0,
|
|
15
|
+
flags: [0, 0, 0],
|
|
16
|
+
entries_count: matrix.size,
|
|
17
|
+
matrix: matrix,
|
|
18
|
+
).to_binary_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def matrix
|
|
22
|
+
@matrix ||= cache.elst_entries(track_id).map do |entry|
|
|
23
|
+
{
|
|
24
|
+
segment_duration: 0,
|
|
25
|
+
media_time: entry.fetch('media_time'),
|
|
26
|
+
media_rate: entry.fetch('media_rate'),
|
|
27
|
+
media_rate_fraction: entry.fetch('media_rate_fraction'),
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Ftyp < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
def name
|
|
6
|
+
'ftyp'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def binary_content
|
|
10
|
+
@binary_content ||= MP4::Binary::Ftyp.new(
|
|
11
|
+
major_brand: memoized_ftyp.fetch('major_brand'),
|
|
12
|
+
minor_version: memoized_ftyp.fetch('minor_version'),
|
|
13
|
+
compatible_brands: memoized_ftyp.fetch('compatible_brands'),
|
|
14
|
+
).to_binary_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def memoized_ftyp
|
|
18
|
+
@memoized_ftyp ||= cache.ftyp
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module M4S
|
|
2
|
+
module Layout
|
|
3
|
+
class Hdlr < Atom
|
|
4
|
+
using ::MP4::Ext::Refinements
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
'hdlr'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def binary_content
|
|
13
|
+
@binary_content ||= ::MP4::Binary::Hdlr.new(
|
|
14
|
+
version: 0,
|
|
15
|
+
flags: [0, 0, 0],
|
|
16
|
+
handler_type: track.fetch('handler_type'),
|
|
17
|
+
handler_name: track.fetch('handler_name'),
|
|
18
|
+
).to_binary_s
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
module InitMp4Builder
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def call(cache, track_id)
|
|
6
|
+
# binding.pry
|
|
7
|
+
track = cache.track(track_id)
|
|
8
|
+
|
|
9
|
+
container = Container.new(root: true)
|
|
10
|
+
|
|
11
|
+
container.add_atom Ftyp.generate(cache)
|
|
12
|
+
|
|
13
|
+
container.add_container :moov do |moov|
|
|
14
|
+
moov.add_atom Mvhd.generate(cache)
|
|
15
|
+
|
|
16
|
+
moov.add_container :trak do |trak|
|
|
17
|
+
trak.add_atom Tkhd.generate(cache, track: track)
|
|
18
|
+
|
|
19
|
+
trak.add_container :edts do |edts|
|
|
20
|
+
edts.add_atom Elst.generate(cache, track: track)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
trak.add_container :mdia do |mdia|
|
|
24
|
+
# if track.fetch('handler_type') == 'vide'
|
|
25
|
+
mdia.add_atom Mdhd.generate(cache, track: track)
|
|
26
|
+
mdia.add_atom Hdlr.generate(cache, track: track)
|
|
27
|
+
|
|
28
|
+
mdia.add_container :minf do |minf|
|
|
29
|
+
minf.add_atom VSmhd.generate(cache, track: track)
|
|
30
|
+
minf.add_atom Dinf.generate(cache, track: track)
|
|
31
|
+
|
|
32
|
+
minf.add_container :stbl do |stbl|
|
|
33
|
+
stbl.add_atom Stsd.generate(cache, track: track)
|
|
34
|
+
stbl.add_atom Stts.generate(cache, track: track)
|
|
35
|
+
|
|
36
|
+
if track['handler_type'] == 'vide'
|
|
37
|
+
stbl.add_atom Stss.generate(cache, track: track)
|
|
38
|
+
stbl.add_atom Ctts.generate(cache, track: track) if track['have_composition_time'].nonzero?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
stbl.add_atom Stsc.generate(cache, track: track)
|
|
42
|
+
stbl.add_atom Stsz.generate(cache, track: track)
|
|
43
|
+
stbl.add_atom Stco.generate(cache, track: track)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
moov.add_container :mvex do |mvex|
|
|
50
|
+
mvex.add_atom Trex.generate(cache)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
container
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Mdat < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
attr_reader :source, :track_id
|
|
6
|
+
|
|
7
|
+
def initialize(cache, opts = {})
|
|
8
|
+
super(cache, opts)
|
|
9
|
+
@source = opts.fetch(:source)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def name
|
|
15
|
+
'mdat'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def binary_content
|
|
19
|
+
return @binary_content if defined?(@binary_content)
|
|
20
|
+
|
|
21
|
+
buffer = StringIO.new
|
|
22
|
+
|
|
23
|
+
source.reset_seek
|
|
24
|
+
|
|
25
|
+
samples = cache.segment_bytes_sizes_offsets(track.fetch('track_id'), segment_index)
|
|
26
|
+
|
|
27
|
+
if source.respond_to?(:fetch)
|
|
28
|
+
range(buffer, samples)
|
|
29
|
+
else
|
|
30
|
+
progressive(buffer, samples)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
buffer.seek(0)
|
|
34
|
+
|
|
35
|
+
@binary_content = BinData::String.new(buffer.read).to_binary_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def range(buffer, samples)
|
|
39
|
+
range_from = samples.first.fetch('offset')
|
|
40
|
+
range_to = samples.last.fetch('offset') + samples.last.fetch('size')
|
|
41
|
+
bytes = StringIO.new(source.fetch(range_from, range_to))
|
|
42
|
+
|
|
43
|
+
samples.each do |chunk|
|
|
44
|
+
offset = chunk.fetch('offset') - range_from
|
|
45
|
+
bytes.seek(offset)
|
|
46
|
+
chunk_bytes = bytes.read(chunk.fetch('size'))
|
|
47
|
+
|
|
48
|
+
buffer.write chunk_bytes
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def progressive(buffer, samples)
|
|
53
|
+
samples.each do |chunk|
|
|
54
|
+
source.set_seek chunk.fetch('offset')
|
|
55
|
+
chunk_bytes = source.read(chunk.fetch('size'))
|
|
56
|
+
|
|
57
|
+
buffer.write chunk_bytes
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module M4S
|
|
2
|
+
module Layout
|
|
3
|
+
class Mdhd < Atom
|
|
4
|
+
using ::MP4::Ext::Refinements
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
'mdhd'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def binary_content
|
|
13
|
+
@binary_content ||= ::MP4::Binary::Mdhd.new(
|
|
14
|
+
version: 0,
|
|
15
|
+
flags: [0, 0, 0],
|
|
16
|
+
creation_time: 0,
|
|
17
|
+
modification_time: 0,
|
|
18
|
+
timescale: track.fetch('timescale'),
|
|
19
|
+
duration: 0,
|
|
20
|
+
language: [0, 0, 0],
|
|
21
|
+
).to_binary_s
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Mfhd < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
def name
|
|
6
|
+
'mfhd'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def binary_content
|
|
10
|
+
@binary_content ||= M4S::Binary::Mfhd.new(
|
|
11
|
+
version: 0,
|
|
12
|
+
flags: 0,
|
|
13
|
+
sequence_number: segment_index,
|
|
14
|
+
).to_binary_s
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Mvhd < Atom
|
|
3
|
+
DEFAULT_TIMESCALE = 1000
|
|
4
|
+
|
|
5
|
+
using ::MP4::Ext::Refinements
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def name
|
|
10
|
+
'mvhd'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def binary_content
|
|
14
|
+
@binary_content ||= MP4::Binary::Mvhd.new(
|
|
15
|
+
version: 0,
|
|
16
|
+
flags: [0, 0, 0],
|
|
17
|
+
creation_time: 0,
|
|
18
|
+
modification_time: 0,
|
|
19
|
+
timescale: DEFAULT_TIMESCALE,
|
|
20
|
+
duration: 0,
|
|
21
|
+
rate: 1.0,
|
|
22
|
+
volume: 1.0,
|
|
23
|
+
matrix: [65536, 0, 0, 0, 65536, 0, 0, 0, 1073741824],
|
|
24
|
+
next_track_id: calculate_next_track_id,
|
|
25
|
+
).to_binary_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def calculate_next_track_id
|
|
29
|
+
cache.next_track_id
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module M4S::Layout
|
|
2
|
+
class Sidx < Atom
|
|
3
|
+
using ::MP4::Ext::Refinements
|
|
4
|
+
|
|
5
|
+
def name
|
|
6
|
+
'sidx'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def binary_content
|
|
10
|
+
@binary_content ||= M4S::Binary::Sidx.new(
|
|
11
|
+
version: 1,
|
|
12
|
+
flags: 0,
|
|
13
|
+
reference_id: 1,
|
|
14
|
+
timescale: track.fetch('timescale'),
|
|
15
|
+
earliest_presentation_time: earliest_presentation_time,
|
|
16
|
+
first_offset: 0,
|
|
17
|
+
reference_count: 1,
|
|
18
|
+
references: [
|
|
19
|
+
{ reference_type: 0, referenced_size: segment_size, subsegment_duration: segment_duration, starts_with_sap: 1, sap_type: 0, sap_delta_time: 0 },
|
|
20
|
+
],
|
|
21
|
+
).to_binary_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def segment_duration
|
|
25
|
+
cache.segment_duration_ticks(track.fetch('track_id'), segment_index)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def earliest_presentation_time
|
|
29
|
+
cache.segment_earliest_presentation_ticks(track.fetch('track_id'), segment_index)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def segment_size
|
|
33
|
+
cache.segment_size_bytes(track.fetch('track_id'), segment_index)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|