ffmprb 0.7.5 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -4,208 +4,67 @@ module Ffmprb
4
4
 
5
5
  class Input
6
6
 
7
- class Cropped < Input
7
+ class << self
8
8
 
9
- attr_reader :crop_ratios
9
+ def resolve(io)
10
+ return io unless io.is_a? String
10
11
 
11
- def initialize(unfiltered, crop:)
12
- @io = unfiltered
13
- self.crop_ratios = crop
14
- end
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
- class Cut < Input
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
- ['-i', @io.path]
138
- end
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 volume(vol)
187
- Loud.new self, volume: vol
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, force=false)
191
- return !!@channels && @channels.include?(medium) && @io.channel?(medium) if force
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
- case io
202
- when /^\/\w/
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