ffmprb 0.6.6

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,211 @@
1
+ module Ffmprb
2
+
3
+ module Util
4
+
5
+ # XXX the events mechanism is currently unused (and commented out) => synchro mechanism not needed
6
+ # XXX partially specc'ed in file_spec
7
+ class IoBuffer
8
+ # include Synchro
9
+
10
+ class << self
11
+
12
+ attr_accessor :blocks_max
13
+ attr_accessor :block_size
14
+ attr_accessor :timeout
15
+
16
+ def default_size
17
+ blocks_max * block_size
18
+ end
19
+
20
+ end
21
+
22
+ # NOTE input/output can be lambdas for single asynchronic io evaluation
23
+ # NOTE both ios are closed as soon as possible
24
+ def initialize(input, output,
25
+ blocks_max: self.class.blocks_max, block_size: self.class.block_size)
26
+
27
+ @input = input
28
+ @output = output
29
+ @q = SizedQueue.new(blocks_max)
30
+ @stat_blocks_max = 0
31
+ @terminate = false
32
+ # @events = {}
33
+
34
+ # NOTE reads all of input, then closes the stream times out on buffer overflow
35
+
36
+ @reader = Util::Thread.new("buffer reader") do
37
+ begin
38
+ while s = reader_input!.read(block_size)
39
+ begin
40
+ Timeout::timeout(self.class.timeout) do
41
+ @q.enq s
42
+ end
43
+ rescue Timeout::Error # NOTE the queue is probably overflown
44
+ @terminate = Error.new("The reader has failed with timeout while queuing")
45
+ # timeout!
46
+ raise Error, "Looks like we're stuck (#{self.class.timeout}s idle) with #{blocks_max}x#{block_size}B blocks (buffering #{reader_input!.path}->...)..."
47
+ end
48
+ @stat_blocks_max = blocks_count if blocks_count > @stat_blocks_max
49
+ end
50
+ @terminate = true
51
+ @q.enq nil
52
+ ensure
53
+ begin
54
+ reader_input!.close if reader_input!.respond_to?(:close)
55
+ rescue
56
+ Ffmprb.logger.error "IoBuffer input closing error: #{$!.message}"
57
+ end
58
+ # reader_done!
59
+ Ffmprb.logger.debug "IoBuffer reader terminated (blocks max: #{@stat_blocks_max})"
60
+ end
61
+ end
62
+
63
+ init_writer_output!
64
+
65
+ # NOTE writes as much output as possible, then terminates when the reader dies
66
+
67
+ @writer = Util::Thread.new("buffer writer") do
68
+ broken = false
69
+ begin
70
+ while s = @q.deq
71
+ next if broken
72
+ written = 0
73
+ while !broken
74
+ raise @terminate if @terminate.kind_of?(Exception)
75
+ begin
76
+ output = writer_output!
77
+ written = output.write_nonblock(s) if output # NOTE will only be nil if @terminate is an exception
78
+ break if written == s.length # NOTE kinda optimisation
79
+ s = s[written..-1]
80
+ rescue Errno::EAGAIN
81
+ Ffmprb.logger.debug "IoBuffer writer (to #{output.path}) retrying"
82
+ sleep 0.01
83
+ rescue Errno::EWOULDBLOCK
84
+ Ffmprb.logger.debug "IoBuffer writer (to #{output.path}) taking a break"
85
+ sleep 0.01
86
+ rescue Errno::EPIPE
87
+ broken = true
88
+ Ffmprb.logger.debug "IoBuffer writer (to #{output.path}) broken"
89
+ end
90
+ end
91
+ end
92
+ ensure
93
+ # terminated!
94
+ begin
95
+ writer_output!.close if !broken && writer_output!.respond_to?(:close)
96
+ rescue
97
+ Ffmprb.logger.error "IoBuffer output closing error: #{$!.message}"
98
+ end
99
+ Ffmprb.logger.debug "IoBuffer writer terminated (blocks max: #{@stat_blocks_max})"
100
+ end
101
+ end
102
+ end
103
+
104
+ def flush! # NOTE blocking, closes ios
105
+ e = nil
106
+ # [@reader, @writer, @handler_thr].each do |thr|
107
+ [@reader, @writer, @output_thr].compact.each do |thr|
108
+ begin
109
+ thr.join
110
+ rescue
111
+ if e
112
+ Ffmprb.logger.debug "Additionally got (hidden): #{$!.message}"
113
+ else
114
+ e = $!
115
+ end
116
+ end
117
+ end
118
+ raise e if e
119
+ end
120
+ # handle_synchronously :flush!
121
+ #
122
+ # def once(event, &blk)
123
+ # event = event.to_sym
124
+ # wait_for_handler!
125
+ # if @events[event].respond_to? :call
126
+ # raise Error, "Once upon a time (one #once(event) at a time) please"
127
+ # elsif @events[event]
128
+ # Ffmprb.logger.debug "IoBuffer (post-)reacting to #{event}"
129
+ # @handler_thr = Util::Thread.new "#{event} handler", &blk
130
+ # else
131
+ # Ffmprb.logger.debug "IoBuffer subscribing to #{event}"
132
+ # @events[event] = blk
133
+ # end
134
+ # end
135
+ # handle_synchronously :once
136
+ #
137
+ # def reader_done!
138
+ # Ffmprb.logger.debug "IoBuffer reader terminated (blocks max: #{@stat_blocks_max})"
139
+ # fire! :reader_done
140
+ # end
141
+ #
142
+ # def terminated!
143
+ # fire! :terminated
144
+ # end
145
+ #
146
+ # def timeout!
147
+ # fire! :timeout
148
+ # end
149
+
150
+ protected
151
+ #
152
+ # def fire!(event)
153
+ # wait_for_handler!
154
+ # Ffmprb.logger.debug "IoBuffer firing #{event}"
155
+ # if blk = @events.to_h[event.to_sym]
156
+ # @handler_thr = Util::Thread.new "#{event} handler", &blk
157
+ # end
158
+ # @events[event.to_sym] = true
159
+ # end
160
+ # handle_synchronously :fire!
161
+ #
162
+ def blocks_count
163
+ @q.size
164
+ end
165
+
166
+ private
167
+
168
+ def reader_input! # NOTE just for reader thread
169
+ if @input.respond_to?(:call)
170
+ Ffmprb.logger.debug "Opening buffer input"
171
+ @input = @input.call
172
+ Ffmprb.logger.debug "Opened buffer input: #{@input.path}"
173
+ end
174
+ @input
175
+ end
176
+
177
+ def writer_output! # NOTE just for writer thread
178
+ if @output_thr
179
+ @output_thr.join
180
+ @output_thr = nil
181
+ end
182
+ @output unless @output.respond_to?(:call)
183
+ end
184
+
185
+ def init_writer_output!
186
+ return unless @output.respond_to?(:call)
187
+
188
+ @output_thr = Util::Thread.new("buffer writer output helper") do
189
+ Ffmprb.logger.debug "Opening buffer output"
190
+ begin
191
+ Timeout::timeout(self.class.timeout/2) do
192
+ @output = @output.call
193
+ Ffmprb.logger.debug "Opened buffer output: #{@output.path}"
194
+ end
195
+ rescue Timeout::Error
196
+ Ffmprb.logger.info "A little bit of timeout in the buffer writer helper thread"
197
+ retry unless @terminate.kind_of?(Exception)
198
+ end
199
+ end
200
+ end
201
+ #
202
+ # def wait_for_handler!
203
+ # @handler_thr.join if @handler_thr
204
+ # @handler_thr = nil
205
+ # end
206
+
207
+ end
208
+
209
+ end
210
+
211
+ end
@@ -0,0 +1,47 @@
1
+ # XXX unused and commented out
2
+ # module Ffmprb
3
+ #
4
+ # module Util
5
+ #
6
+ # # NOTE doesn't have specs (and not too proud about it)
7
+ # module Synchro
8
+ #
9
+ # module ClassMethods
10
+ #
11
+ # def handle_synchronously(*methods)
12
+ # prepend Module.new do
13
+ #
14
+ # methods.each do |method|
15
+ #
16
+ # define_method method do
17
+ # @_synchro.synchronize do
18
+ # super
19
+ # end
20
+ # end
21
+ #
22
+ # end
23
+ #
24
+ # end
25
+ # end
26
+ #
27
+ # end
28
+ #
29
+ # module InstanceMethods
30
+ #
31
+ # def initialize(*args)
32
+ # @_synchro = Monitor.new
33
+ # super
34
+ # end
35
+ #
36
+ # end
37
+ #
38
+ # def self.included(mod)
39
+ # mod.prepend InstanceMethods
40
+ # mod.extend ClassMethods
41
+ # end
42
+ #
43
+ # end
44
+ #
45
+ # end
46
+ #
47
+ # end
@@ -0,0 +1,28 @@
1
+ module Ffmprb
2
+
3
+ module Util
4
+
5
+ # NOTE doesn't have specs (and not too proud about it)
6
+ class Thread < ::Thread
7
+
8
+ def initialize(name="some", &blk)
9
+ super() do
10
+ begin
11
+ Ffmprb.logger.debug "#{name} thread launched"
12
+ blk.call
13
+ Ffmprb.logger.debug "#{name} thread done"
14
+ rescue Exception
15
+ Ffmprb.logger.warn "#{$!.class} caught in a #{name} thread (hidden): #{$!.message}\nBacktrace:\n\t#{$!.backtrace.join("\n\t")}"
16
+ cause = $!
17
+ Ffmprb.logger.warn "...caused by #{cause.class}: #{cause.message}\nBacktrace:\n\t#{cause.backtrace.join("\n\t")}" while
18
+ cause = cause.cause
19
+ raise
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,89 @@
1
+ # require 'ffmprb/util/synchro'
2
+ require 'ffmprb/util/thread'
3
+ require 'ffmprb/util/io_buffer'
4
+
5
+ require 'open3'
6
+
7
+ module Ffmprb
8
+
9
+ module Util
10
+
11
+ class << self
12
+
13
+ def ffprobe(args)
14
+ sh "ffprobe#{args}"
15
+ end
16
+
17
+ def ffmpeg(args)
18
+ args = " -loglevel debug#{args}" if Ffmprb.debug
19
+ sh "ffmpeg -y#{args}", output: :stderr
20
+ end
21
+
22
+ def sh(cmd, output: :stdout, log: :stderr)
23
+ Ffmprb.logger.info cmd
24
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
25
+ stdin.close
26
+
27
+ # XXX process timeouting/cleanup here will be appreciated
28
+
29
+ begin
30
+ log_cmd = "#{cmd.split(' ').first.upcase}: " if log
31
+ stdout_r = Reader.new(stdout, output == :stdout, log == :stdout && log_cmd)
32
+ stderr_r = Reader.new(stderr, true, log == :stderr && log_cmd)
33
+
34
+ raise Error, "#{cmd}:\n#{stderr_r.read}" unless
35
+ wait_thr.value.exitstatus == 0 # NOTE blocks
36
+
37
+ # NOTE only one of them will return non-nil, see above
38
+ stdout_r.read || stderr_r.read
39
+ ensure
40
+ begin
41
+ stdout_r.join if stdout_r
42
+ stdout_r = nil
43
+ stderr_r.join if stderr_r
44
+ rescue
45
+ Ffmprb.logger.error "Thread joining error: #{$!.message}"
46
+ stderr_r.join if stdout_r
47
+ end
48
+ Ffmprb.logger.debug "FINISHED: #{cmd}"
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+
56
+ class Reader < Thread
57
+
58
+ def initialize(input, store=false, log=nil)
59
+ @output = ''
60
+ @queue = Queue.new
61
+ super "reader" do
62
+ begin
63
+ while s = input.gets
64
+ Ffmprb.logger.debug log + s.chomp if log
65
+ @output << s if store
66
+ end
67
+ @queue.enq @output
68
+ rescue Exception
69
+ @queue.enq Error.new("Exception in a reader thread")
70
+ end
71
+ end
72
+ end
73
+
74
+ def read
75
+ case res = @queue.deq
76
+ when Exception
77
+ raise res
78
+ when ''
79
+ nil
80
+ else
81
+ res
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,3 @@
1
+ module Ffmprb
2
+ VERSION = "0.6.6"
3
+ end
data/lib/ffmprb.rb ADDED
@@ -0,0 +1,83 @@
1
+ require 'ffmprb/file'
2
+ require 'ffmprb/filter'
3
+ require 'ffmprb/process'
4
+ require 'ffmprb/util'
5
+ require 'ffmprb/version'
6
+
7
+ require 'logger'
8
+ require 'time'
9
+ require 'timeout'
10
+
11
+ module Ffmprb
12
+
13
+ ENV_VAR_FALSE_REGEX = /^(0|no?|false)?$/i
14
+
15
+ QVGA = '320x240'
16
+ HD_720p = '1280x720'
17
+ HD_1080p = '1920x1080'
18
+
19
+ class Error < StandardError
20
+ end
21
+
22
+ Util::IoBuffer.blocks_max = 1024
23
+ Util::IoBuffer.block_size = 64*1024
24
+ Util::IoBuffer.timeout = 9
25
+
26
+ class << self
27
+
28
+ def process(*args, &blk)
29
+ logger.debug "Starting process with #{args} in #{blk.source_location}"
30
+ Process.new.tap do |process|
31
+ if blk
32
+ process.instance_exec *args, &blk
33
+ process.run
34
+ logger.debug "Finished process with #{args} in #{blk.source_location}"
35
+ end
36
+ end
37
+ end
38
+ alias :action! :process # ;)
39
+
40
+ attr_accessor :debug
41
+
42
+ def logger
43
+ @logger ||= Logger.new(STDERR).tap do |logger|
44
+ logger.level = debug ? Logger::DEBUG : Logger::INFO
45
+ end
46
+ end
47
+
48
+ def logger=(logger)
49
+ @logger.close if @logger
50
+ @logger = logger
51
+ end
52
+
53
+ def find_silence(input_file, output_file)
54
+ logger.debug "Finding silence (#{input_file.path}->#{output_file.path})"
55
+ filters = Filter.silencedetect
56
+ options = " -i #{input_file.path}#{Filter.complex_options filters} #{output_file.path}"
57
+ silence = []
58
+ Util.ffmpeg(options).split("\n").each do |line|
59
+ next unless line =~ /^\[silencedetect\s.*\]\s*silence_(\w+):\s*(\d+\.?d*)/
60
+ case $1
61
+ when 'start'
62
+ silence << OpenStruct.new(start_at: $2.to_f)
63
+ when 'end'
64
+ if silence.empty?
65
+ silence << OpenStruct.new(start_at: 0.0, end_at: $2.to_f)
66
+ else
67
+ raise Error, "ffmpeg is being stupid: silence_end with no silence_start" if silence.last.end_at
68
+ silence.last.end_at = $2.to_f
69
+ end
70
+ else
71
+ Ffmprb.warn "Unknown silence mark: #{$1}"
72
+ end
73
+ end
74
+
75
+ logger.debug "Found silence (#{input_file.path}->#{output_file.path}): [#{silence.map{|t,v| "#{t}: #{v}"}}]"
76
+ silence
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ Ffmprb.debug = ENV.fetch('FFMPRB_DEBUG', '') !~ Ffmprb::ENV_VAR_FALSE_REGEX
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffmprb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.6
5
+ platform: ruby
6
+ authors:
7
+ - showbox.com
8
+ - Costa Shapiro @ Showbox
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2015-07-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mkfifo
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 1.9.9
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 1.9.9
42
+ - !ruby/object:Gem::Dependency
43
+ name: byebug
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 4.0.5
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 4.0.5
56
+ - !ruby/object:Gem::Dependency
57
+ name: guard-rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.12.8
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 2.12.8
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 10.4.2
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 10.4.2
84
+ - !ruby/object:Gem::Dependency
85
+ name: rmagick
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '2.15'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '2.15'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rspec
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 3.2.0
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.2.0
112
+ - !ruby/object:Gem::Dependency
113
+ name: ruby-sox
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 0.0.3
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 0.0.3
126
+ description: A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends
127
+ email:
128
+ - costa@showbox.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".ruby-version"
136
+ - ".travis.yml"
137
+ - Gemfile
138
+ - Guardfile
139
+ - README.md
140
+ - Rakefile
141
+ - bin/console
142
+ - bin/guard
143
+ - bin/rake
144
+ - bin/rspec
145
+ - bin/setup
146
+ - ffmprb.gemspec
147
+ - lib/ffmprb.rb
148
+ - lib/ffmprb/file.rb
149
+ - lib/ffmprb/filter.rb
150
+ - lib/ffmprb/process.rb
151
+ - lib/ffmprb/process/input.rb
152
+ - lib/ffmprb/process/output.rb
153
+ - lib/ffmprb/util.rb
154
+ - lib/ffmprb/util/io_buffer.rb
155
+ - lib/ffmprb/util/synchro.rb
156
+ - lib/ffmprb/util/thread.rb
157
+ - lib/ffmprb/version.rb
158
+ homepage: https://github.com/showbox-oss/ffmprb
159
+ licenses: []
160
+ metadata: {}
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ requirements: []
176
+ rubyforge_project:
177
+ rubygems_version: 2.4.6
178
+ signing_key:
179
+ specification_version: 4
180
+ summary: ffmprb is your audio/video montage friend, based on https://ffmpeg.org
181
+ test_files: []