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.
@@ -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