ffmprb 0.7.5 → 0.9.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 +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
|