ffmprb 0.7.5 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Guardfile +2 -2
- data/README.md +121 -15
- data/ffmprb.gemspec +2 -1
- data/lib/defaults.rb +5 -1
- data/lib/ffmprb/file.rb +2 -4
- data/lib/ffmprb/filter.rb +97 -65
- data/lib/ffmprb/process/input/chain_base.rb +29 -0
- data/lib/ffmprb/process/input/channeled.rb +40 -0
- data/lib/ffmprb/process/input/cropped.rb +70 -0
- data/lib/ffmprb/process/input/cut.rb +66 -0
- data/lib/ffmprb/process/input/looping.rb +102 -0
- data/lib/ffmprb/process/input/loud.rb +42 -0
- data/lib/ffmprb/process/input/temp.rb +26 -0
- data/lib/ffmprb/process/input.rb +39 -180
- data/lib/ffmprb/process/output.rb +140 -119
- data/lib/ffmprb/process.rb +68 -27
- data/lib/ffmprb/util/synchro.rb +1 -1
- data/lib/ffmprb/util/thread.rb +2 -2
- data/lib/ffmprb/util/threaded_io_buffer.rb +48 -36
- data/lib/ffmprb/util.rb +12 -5
- data/lib/ffmprb/version.rb +1 -1
- data/lib/ffmprb.rb +6 -6
- metadata +9 -2
@@ -0,0 +1,66 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
def cut(from: 0, to: nil)
|
8
|
+
Cut.new self, from: from, to: to
|
9
|
+
end
|
10
|
+
|
11
|
+
class Cut < ChainBase
|
12
|
+
|
13
|
+
attr_reader :from, :to
|
14
|
+
|
15
|
+
def initialize(unfiltered, from:, to:)
|
16
|
+
super unfiltered
|
17
|
+
@from = from
|
18
|
+
@to = to.to_f == 0 ? nil : to
|
19
|
+
|
20
|
+
fail Error, "cut from: must be" unless from
|
21
|
+
fail Error, "cut from: must be less than to:" unless !to || from < to
|
22
|
+
end
|
23
|
+
|
24
|
+
def filters_for(lbl, video:, audio:)
|
25
|
+
fail Error, "cut needs resolution and fps (reorder your filters?)" unless
|
26
|
+
!video || video.resolution && video.fps
|
27
|
+
|
28
|
+
# Trimming
|
29
|
+
|
30
|
+
lbl_aux = "tm#{lbl}"
|
31
|
+
unfiltered.filters_for(lbl_aux, video: video, audio: audio) +
|
32
|
+
if to
|
33
|
+
lbl_blk = "bl#{lbl}"
|
34
|
+
lbl_pad = "pd#{lbl}"
|
35
|
+
[
|
36
|
+
*((video && channel?(:video))?
|
37
|
+
Filter.blank_source(to - from, video.resolution, video.fps, "#{lbl_blk}:v") +
|
38
|
+
Filter.concat_v(["#{lbl_aux}:v", "#{lbl_blk}:v"], "#{lbl_pad}:v") +
|
39
|
+
Filter.trim(from, to, "#{lbl_pad}:v", "#{lbl}:v")
|
40
|
+
: nil),
|
41
|
+
*((audio && channel?(:audio))?
|
42
|
+
Filter.silent_source(to - from, "#{lbl_blk}:a") +
|
43
|
+
Filter.concat_a(["#{lbl_aux}:a", "#{lbl_blk}:a"], "#{lbl_pad}:a") +
|
44
|
+
Filter.atrim(from, to, "#{lbl_pad}:a", "#{lbl}:a")
|
45
|
+
: nil)
|
46
|
+
]
|
47
|
+
elsif from == 0
|
48
|
+
[
|
49
|
+
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
|
50
|
+
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
51
|
+
]
|
52
|
+
else # !to
|
53
|
+
[
|
54
|
+
*((video && channel?(:video))? Filter.trim(from, nil, "#{lbl_aux}:v", "#{lbl}:v"): nil),
|
55
|
+
*((audio && channel?(:audio))? Filter.atrim(from, nil, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
56
|
+
]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
def loop(times=31)
|
8
|
+
Looping.new self, times
|
9
|
+
end
|
10
|
+
|
11
|
+
class Looping < ChainBase
|
12
|
+
|
13
|
+
attr_reader :times
|
14
|
+
|
15
|
+
def initialize(unfiltered, times)
|
16
|
+
super unfiltered
|
17
|
+
@times = times
|
18
|
+
|
19
|
+
@raw = unfiltered
|
20
|
+
@raw = @raw.unfiltered while @raw.respond_to? :unfiltered
|
21
|
+
@src_io = @raw.io
|
22
|
+
@raw.temporise!
|
23
|
+
@aux_input = @raw.process.temp_input(@src_io.extname)
|
24
|
+
end
|
25
|
+
|
26
|
+
def filters_for(lbl, video:, audio:)
|
27
|
+
|
28
|
+
# Looping
|
29
|
+
|
30
|
+
loop_unfiltered(video: video, audio: audio).filters_for lbl,
|
31
|
+
video: OpenStruct.new, audio: OpenStruct.new # NOTE the processing is done before looping
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def loop_unfiltered(video:, audio:)
|
37
|
+
fail Error, "Double looping is not supported... yet" unless @src_io # TODO video & audio params check
|
38
|
+
src_io = @src_io
|
39
|
+
@src_io = nil
|
40
|
+
|
41
|
+
Ffmprb.logger.debug "Validating limitations..."
|
42
|
+
|
43
|
+
raw = unfiltered
|
44
|
+
raw = raw.unfiltered while raw.respond_to? :unfiltered
|
45
|
+
fail Error, "Something is wrong (double looping?)" unless raw == @raw
|
46
|
+
|
47
|
+
dst_io = File.temp_fifo(src_io.extname)
|
48
|
+
|
49
|
+
buff_raw_io = File.temp_fifo(src_io.extname)
|
50
|
+
Util::ThreadedIoBuffer.new(
|
51
|
+
File.async_opener(buff_raw_io, 'r'),
|
52
|
+
File.async_opener(raw.io, 'w')
|
53
|
+
)
|
54
|
+
|
55
|
+
Ffmprb.logger.debug "Preprocessed looping input will be #{dst_io.path} and raw input copy will go through #{buff_raw_io.path} to #{raw.io.path}..."
|
56
|
+
|
57
|
+
Util::Thread.new "looping input processor" do
|
58
|
+
Ffmprb.logger.debug "Processing before looping"
|
59
|
+
|
60
|
+
process = Process.new
|
61
|
+
in1 = process.input(src_io)
|
62
|
+
process.output(dst_io, video: video, audio: audio).
|
63
|
+
lay in1.copy(unfiltered)
|
64
|
+
process.output(buff_raw_io,
|
65
|
+
video: OpenStruct.new, audio: OpenStruct.new # NOTE raw input copy
|
66
|
+
).
|
67
|
+
lay in1
|
68
|
+
process.run # TODO limit:
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
buff_ios = (0..times).map{File.temp_fifo src_io.extname}
|
73
|
+
Ffmprb.logger.debug "Preprocessed #{dst_io.path} will be teed to #{buff_ios.map(&:path).join '; '}"
|
74
|
+
Util::ThreadedIoBuffer.new(
|
75
|
+
File.async_opener(dst_io, 'r'),
|
76
|
+
*buff_ios.map{|io| File.async_opener io, 'w'}
|
77
|
+
)
|
78
|
+
|
79
|
+
Ffmprb.logger.debug "Concatenation of #{buff_ios.map(&:path).join '; '} will go to #{@aux_input.io.path} to be fed to this process"
|
80
|
+
|
81
|
+
Util::Thread.new "looper" do
|
82
|
+
Ffmprb.logger.debug "Looping #{buff_ios.size} times"
|
83
|
+
|
84
|
+
process = Process.new(ignore_broken_pipe: true) # NOTE may not write its entire output, it's ok
|
85
|
+
ins = buff_ios.map{|i| process.input i}
|
86
|
+
process.output(@aux_input.io, video: nil, audio: nil) do
|
87
|
+
ins.each{|i| lay i}
|
88
|
+
end
|
89
|
+
process.run # TODO limit:
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
self.unfiltered = @aux_input
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
def mute
|
8
|
+
Loud.new self, volume: 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def volume(vol)
|
12
|
+
Loud.new self, volume: vol
|
13
|
+
end
|
14
|
+
|
15
|
+
class Loud < ChainBase
|
16
|
+
|
17
|
+
def initialize(unfiltered, volume:)
|
18
|
+
super unfiltered
|
19
|
+
@volume = volume
|
20
|
+
|
21
|
+
fail Error, "volume cannot be nil" if volume.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
def filters_for(lbl, video:, audio:)
|
25
|
+
|
26
|
+
# Modulating volume
|
27
|
+
|
28
|
+
lbl_aux = "ld#{lbl}"
|
29
|
+
unfiltered.filters_for(lbl_aux, video: video, audio: audio) +
|
30
|
+
[
|
31
|
+
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
|
32
|
+
*((audio && channel?(:audio))? Filter.volume(@volume, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
def temporise!(extname=nil)
|
8
|
+
extname ||= io.extname
|
9
|
+
self.io = nil
|
10
|
+
extend Temp
|
11
|
+
@extname = extname
|
12
|
+
end
|
13
|
+
|
14
|
+
module Temp
|
15
|
+
|
16
|
+
def io
|
17
|
+
@io ||= File.temp_fifo(@extname)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/ffmprb/process/input.rb
CHANGED
@@ -4,208 +4,67 @@ module Ffmprb
|
|
4
4
|
|
5
5
|
class Input
|
6
6
|
|
7
|
-
class
|
7
|
+
class << self
|
8
8
|
|
9
|
-
|
9
|
+
def resolve(io)
|
10
|
+
return io unless io.is_a? String
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
def filters_for(lbl, process:, output:, video: true, audio: true)
|
17
|
-
|
18
|
-
# Cropping
|
19
|
-
|
20
|
-
lbl_aux = "cp#{lbl}"
|
21
|
-
lbl_tmp = "tmp#{lbl}"
|
22
|
-
@io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
|
23
|
-
[
|
24
|
-
*((video && channel?(:video))? [
|
25
|
-
Filter.crop(crop_ratios, "#{lbl_aux}:v", "#{lbl_tmp}:v"),
|
26
|
-
# XXX this fixup is temporary, leads to resolution loss on crop etc...
|
27
|
-
Filter.scale_pad_fps(output.target_width, output.target_height, output.target_fps, "#{lbl_tmp}:v", "#{lbl}:v")
|
28
|
-
]: nil),
|
29
|
-
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
30
|
-
]
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
CROP_PARAMS = %i[top left bottom right width height]
|
36
|
-
|
37
|
-
def crop_ratios=(ratios)
|
38
|
-
@crop_ratios =
|
39
|
-
if ratios.is_a?(Numeric)
|
40
|
-
{top: ratios, left: ratios, bottom: ratios, right: ratios}
|
41
|
-
else
|
42
|
-
ratios
|
43
|
-
end.tap do |ratios| # NOTE validation
|
44
|
-
next unless ratios
|
45
|
-
fail "Allowed crop params are: #{CROP_PARAMS}" unless ratios.respond_to?(:keys) && (ratios.keys - CROP_PARAMS).empty?
|
46
|
-
ratios.each do |key, value|
|
47
|
-
fail Error, "Crop #{key} must be between 0 and 1 (not '#{value}')" unless (0...1).include? value
|
48
|
-
end
|
12
|
+
case io
|
13
|
+
when /^\/\w/
|
14
|
+
File.open(io).tap do |file|
|
15
|
+
Ffmprb.logger.warn "Input file does no exist (#{file.path}), will probably fail" unless file.exist?
|
49
16
|
end
|
17
|
+
else
|
18
|
+
fail Error, "Cannot resolve input: #{io}"
|
19
|
+
end
|
50
20
|
end
|
51
21
|
|
52
22
|
end
|
53
23
|
|
54
|
-
|
55
|
-
|
56
|
-
attr_reader :from, :to
|
57
|
-
|
58
|
-
def initialize(unfiltered, from:, to:)
|
59
|
-
@io = unfiltered
|
60
|
-
@from, @to = from, (to.to_f == 0 ? nil : to)
|
61
|
-
|
62
|
-
fail Error, "cut from: must be" unless from
|
63
|
-
fail Error, "cut from: must be less than to:" unless !to || from < to
|
64
|
-
end
|
65
|
-
|
66
|
-
def filters_for(lbl, process:, output:, video: true, audio: true)
|
67
|
-
|
68
|
-
# Trimming
|
69
|
-
|
70
|
-
lbl_aux = "tm#{lbl}"
|
71
|
-
@io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
|
72
|
-
if to
|
73
|
-
lbl_blk = "bl#{lbl}"
|
74
|
-
lbl_pad = "pd#{lbl}"
|
75
|
-
[
|
76
|
-
*((video && channel?(:video))?
|
77
|
-
Filter.blank_source(to - from, output.target_resolution, output.target_fps, "#{lbl_blk}:v") +
|
78
|
-
Filter.concat_v(["#{lbl_aux}:v", "#{lbl_blk}:v"], "#{lbl_pad}:v") +
|
79
|
-
Filter.trim(from, to, "#{lbl_pad}:v", "#{lbl}:v")
|
80
|
-
: nil),
|
81
|
-
*((audio && channel?(:audio))?
|
82
|
-
Filter.silent_source(to - from, "#{lbl_blk}:a") +
|
83
|
-
Filter.concat_a(["#{lbl_aux}:a", "#{lbl_blk}:a"], "#{lbl_pad}:a") +
|
84
|
-
Filter.atrim(from, to, "#{lbl_pad}:a", "#{lbl}:a")
|
85
|
-
: nil)
|
86
|
-
]
|
87
|
-
elsif from == 0
|
88
|
-
[
|
89
|
-
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
|
90
|
-
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
91
|
-
]
|
92
|
-
else # !to
|
93
|
-
[
|
94
|
-
*((video && channel?(:video))? Filter.trim(from, nil, "#{lbl_aux}:v", "#{lbl}:v"): nil),
|
95
|
-
*((audio && channel?(:audio))? Filter.atrim(from, nil, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
96
|
-
]
|
97
|
-
end
|
98
|
-
end
|
24
|
+
attr_accessor :io
|
25
|
+
attr_reader :process
|
99
26
|
|
27
|
+
def initialize(io, process)
|
28
|
+
@io = self.class.resolve(io)
|
29
|
+
@process = process
|
100
30
|
end
|
101
31
|
|
102
|
-
class Loud < Input
|
103
|
-
|
104
|
-
attr_reader :from, :to
|
105
|
-
|
106
|
-
def initialize(unfiltered, volume:)
|
107
|
-
@io = unfiltered
|
108
|
-
@volume = volume
|
109
|
-
|
110
|
-
fail Error, "volume cannot be nil" if volume.nil?
|
111
|
-
end
|
112
|
-
|
113
|
-
def filters_for(lbl, process:, output:, video: true, audio: true)
|
114
|
-
|
115
|
-
# Modulating volume
|
116
|
-
|
117
|
-
lbl_aux = "ld#{lbl}"
|
118
|
-
@io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
|
119
|
-
[
|
120
|
-
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
|
121
|
-
*((audio && channel?(:audio))? Filter.volume(@volume, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
122
|
-
]
|
123
|
-
end
|
124
32
|
|
33
|
+
def copy(input)
|
34
|
+
input.chain_copy self
|
125
35
|
end
|
126
36
|
|
127
37
|
|
128
|
-
def initialize(io, only: nil)
|
129
|
-
@io = resolve(io)
|
130
|
-
@channels = [*only]
|
131
|
-
@channels = nil if @channels.empty?
|
132
|
-
raise Error, "Inadequate A/V channels" if
|
133
|
-
[:video, :audio].any?{|medium| !@io.channel?(medium) && channel?(medium, true)}
|
134
|
-
end
|
135
|
-
|
136
38
|
def options
|
137
|
-
[
|
138
|
-
|
139
|
-
|
140
|
-
def filters_for(lbl, process:, output:, video: true, audio: true)
|
141
|
-
|
142
|
-
# Channelling
|
143
|
-
|
144
|
-
if @io.respond_to?(:filters_for)
|
145
|
-
lbl_aux = "au#{lbl}"
|
146
|
-
@io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
|
147
|
-
[
|
148
|
-
*((video && @io.channel?(:video))?
|
149
|
-
(channel?(:video)? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): Filter.nullsink("#{lbl_aux}:v")):
|
150
|
-
nil),
|
151
|
-
*((audio && @io.channel?(:audio))?
|
152
|
-
(channel?(:audio)? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): Filter.anullsink("#{lbl_aux}:a")):
|
153
|
-
nil)
|
154
|
-
]
|
155
|
-
else
|
156
|
-
in_lbl = process[self]
|
157
|
-
raise Error, "Data corruption" unless in_lbl
|
158
|
-
[
|
159
|
-
# XXX this fixup is temporary, leads to resolution loss on crop etc... *(video && @io.channel?(:video) && channel?(:video)? Filter.copy("#{in_lbl}:v", "#{lbl}:v"): nil),
|
160
|
-
*(video && @io.channel?(:video) && channel?(:video)? Filter.scale_pad_fps(output.target_width, output.target_height, output.target_fps, "#{in_lbl}:v", "#{lbl}:v"): nil),
|
161
|
-
*(audio && @io.channel?(:audio) && channel?(:audio)? Filter.anull("#{in_lbl}:a", "#{lbl}:a"): nil)
|
162
|
-
]
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
def video
|
167
|
-
Input.new self, only: :video
|
168
|
-
end
|
169
|
-
|
170
|
-
def audio
|
171
|
-
Input.new self, only: :audio
|
172
|
-
end
|
173
|
-
|
174
|
-
def crop(ratio) # NOTE ratio is either a CROP_PARAMS symbol-ratio hash or a single (global) ratio
|
175
|
-
Cropped.new self, crop: ratio
|
176
|
-
end
|
177
|
-
|
178
|
-
def cut(from: 0, to: nil)
|
179
|
-
Cut.new self, from: from, to: to
|
180
|
-
end
|
181
|
-
|
182
|
-
def mute
|
183
|
-
Loud.new self, volume: 0
|
39
|
+
defaults = %w[-noautorotate -thread_queue_size 32 -i] # TODO parameterise
|
40
|
+
defaults + [io.path]
|
184
41
|
end
|
185
42
|
|
186
|
-
def
|
187
|
-
|
43
|
+
def filters_for(lbl, video:, audio:)
|
44
|
+
in_lbl = process.input_label(self)
|
45
|
+
[
|
46
|
+
*(if video && channel?(:video)
|
47
|
+
if video.resolution && video.fps
|
48
|
+
Filter.scale_pad_fps video.resolution, video.fps, "#{in_lbl}:v", "#{lbl}:v"
|
49
|
+
elsif video.resolution
|
50
|
+
Filter.scale_pad video.resolution, "#{in_lbl}:v", "#{lbl}:v"
|
51
|
+
elsif video.fps
|
52
|
+
Filter.fps video.fps, "#{in_lbl}:v", "#{lbl}:v"
|
53
|
+
else
|
54
|
+
Filter.copy "#{in_lbl}:v", "#{lbl}:v"
|
55
|
+
end
|
56
|
+
end),
|
57
|
+
*(audio && channel?(:audio)? Filter.anull("#{in_lbl}:a", "#{lbl}:a"): nil)
|
58
|
+
]
|
188
59
|
end
|
189
60
|
|
190
|
-
def channel?(medium
|
191
|
-
|
192
|
-
|
193
|
-
(!@channels || @channels.include?(medium)) && @io.channel?(medium)
|
61
|
+
def channel?(medium)
|
62
|
+
io.channel? medium
|
194
63
|
end
|
195
64
|
|
196
|
-
protected
|
197
|
-
|
198
|
-
def resolve(io)
|
199
|
-
return io unless io.is_a? String
|
200
65
|
|
201
|
-
|
202
|
-
|
203
|
-
File.open(io).tap do |file|
|
204
|
-
Ffmprb.logger.warn "Input file does no exist (#{file.path}), will probably fail" unless file.exist?
|
205
|
-
end
|
206
|
-
else
|
207
|
-
fail Error, "Cannot resolve input: #{io}"
|
208
|
-
end
|
66
|
+
def chain_copy(src_input)
|
67
|
+
src_input
|
209
68
|
end
|
210
69
|
|
211
70
|
end
|