ffmprb 0.7.0 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,7 @@ module Ffmprb
4
4
 
5
5
  # XXX the events mechanism is currently unused (and commented out) => synchro mechanism not needed
6
6
  # XXX partially specc'ed in file_spec
7
- class IoBuffer
7
+ class ThreadedIoBuffer
8
8
  # include Synchro
9
9
 
10
10
  class << self
@@ -20,126 +20,43 @@ module Ffmprb
20
20
  end
21
21
 
22
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)
23
+ # the labdas must be timeout-interrupt-safe (since they are wrapped in timeout blocks)
24
+ # NOTE both ios are being opened and closed as soon as possible
25
+ def initialize(input, output)
26
26
 
27
27
  @input = input
28
28
  @output = output
29
- @q = SizedQueue.new(blocks_max)
29
+ @q = SizedQueue.new(self.class.blocks_max)
30
30
  @stat_blocks_max = 0
31
31
  @terminate = false
32
32
  # @events = {}
33
33
 
34
- # NOTE reads all of input, then closes the stream times out on buffer overflow
34
+ Thread.new "io buffer main" do
35
+ init_reader!
36
+ init_writer_output!
37
+ init_writer!
35
38
 
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
- tries = 1
74
- logged_tries = 1/2
75
- while !broken
76
- raise @terminate if @terminate.kind_of?(Exception)
77
- begin
78
- output = writer_output!
79
- written = output.write_nonblock(s) if output # NOTE will only be nil if @terminate is an exception
80
- break if written == s.length # NOTE kinda optimisation
81
- s = s[written..-1]
82
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
83
- if tries == 2 * logged_tries
84
- Ffmprb.logger.debug "IoBuffer writer (to #{output.path}) retrying... (#{tries} writes): #{$!.class}"
85
- logged_tries = tries
86
- end
87
- sleep 0.01
88
- rescue Errno::EPIPE
89
- broken = true
90
- Ffmprb.logger.debug "IoBuffer writer (to #{output.path}) broken"
91
- ensure
92
- tries += 1
93
- end
94
- end
95
- end
96
- ensure
97
- # terminated!
98
- begin
99
- writer_output!.close if !broken && writer_output!.respond_to?(:close)
100
- rescue
101
- Ffmprb.logger.error "IoBuffer output closing error: #{$!.message}"
102
- end
103
- Ffmprb.logger.debug "IoBuffer writer terminated (blocks max: #{@stat_blocks_max})"
104
- end
105
- end
106
- end
107
-
108
- def flush! # NOTE blocking, closes ios
109
- e = nil
110
- # [@reader, @writer, @handler_thr].each do |thr|
111
- [@reader, @writer, @output_thr].compact.each do |thr|
112
- begin
113
- thr.join
114
- rescue
115
- if e
116
- Ffmprb.logger.debug "Additionally got (hidden): #{$!.message}"
117
- else
118
- e = $!
119
- end
120
- end
39
+ Thread.join_children!
121
40
  end
122
- raise e if e
123
41
  end
124
- # handle_synchronously :flush!
125
42
  #
126
43
  # def once(event, &blk)
127
44
  # event = event.to_sym
128
45
  # wait_for_handler!
129
46
  # if @events[event].respond_to? :call
130
- # raise Error, "Once upon a time (one #once(event) at a time) please"
47
+ # fail Error, "Once upon a time (one #once(event) at a time) please"
131
48
  # elsif @events[event]
132
- # Ffmprb.logger.debug "IoBuffer (post-)reacting to #{event}"
49
+ # Ffmprb.logger.debug "ThreadedIoBuffer (post-)reacting to #{event}"
133
50
  # @handler_thr = Util::Thread.new "#{event} handler", &blk
134
51
  # else
135
- # Ffmprb.logger.debug "IoBuffer subscribing to #{event}"
52
+ # Ffmprb.logger.debug "ThreadedIoBuffer subscribing to #{event}"
136
53
  # @events[event] = blk
137
54
  # end
138
55
  # end
139
56
  # handle_synchronously :once
140
57
  #
141
58
  # def reader_done!
142
- # Ffmprb.logger.debug "IoBuffer reader terminated (blocks max: #{@stat_blocks_max})"
59
+ # Ffmprb.logger.debug "ThreadedIoBuffer reader terminated (blocks max: #{@stat_blocks_max})"
143
60
  # fire! :reader_done
144
61
  # end
145
62
  #
@@ -155,7 +72,7 @@ module Ffmprb
155
72
  #
156
73
  # def fire!(event)
157
74
  # wait_for_handler!
158
- # Ffmprb.logger.debug "IoBuffer firing #{event}"
75
+ # Ffmprb.logger.debug "ThreadedIoBuffer firing #{event}"
159
76
  # if blk = @events.to_h[event.to_sym]
160
77
  # @handler_thr = Util::Thread.new "#{event} handler", &blk
161
78
  # end
@@ -189,23 +106,86 @@ module Ffmprb
189
106
  def init_writer_output!
190
107
  return unless @output.respond_to?(:call)
191
108
 
192
- @output_thr = Util::Thread.new("buffer writer output helper") do
109
+ @output_thr = Thread.new("buffer writer output helper") do
193
110
  Ffmprb.logger.debug "Opening buffer output"
194
- tries = 1
195
- logged_tries = 1/2
111
+ @output =
112
+ Thread.timeout_or_live nil, log: "in the buffer writer helper thread", timeout: self.class.timeout do |time|
113
+ fail Error, "giving up buffer writer init since the reader has failed (#{@terminate.message})" if @terminate.kind_of?(Exception)
114
+ @output.call
115
+ end
116
+ Ffmprb.logger.debug "Opened buffer output: #{@output.path}"
117
+ end
118
+ end
119
+
120
+ # NOTE reads all of input, then closes the stream times out on buffer overflow
121
+ def init_reader!
122
+ Thread.new("buffer reader") do
196
123
  begin
197
- Timeout::timeout(self.class.timeout/2) do
198
- @output = @output.call
199
- Ffmprb.logger.debug "Opened buffer output: #{@output.path}"
124
+ while s = reader_input!.read(self.class.block_size)
125
+ begin
126
+ Timeout.timeout(self.class.timeout) do
127
+ @q.enq s
128
+ end
129
+ rescue Timeout::Error # NOTE the queue is probably overflown
130
+ @terminate = Error.new("The reader has failed with timeout while queuing")
131
+ # timeout!
132
+ fail Error, "Looks like we're stuck (#{timeout}s idle) with #{self.class.blocks_max}x#{self.class.block_size}B blocks (buffering #{reader_input!.path}->...)..."
133
+ end
134
+ @stat_blocks_max = blocks_count if blocks_count > @stat_blocks_max
200
135
  end
201
- rescue Timeout::Error
202
- if tries == 2 * logged_tries
203
- Ffmprb.logger.info "A little bit of timeout in the buffer writer helper thread (##{tries})"
204
- logged_tries = tries
136
+ @terminate = true
137
+ @q.enq nil
138
+ ensure
139
+ begin
140
+ reader_input!.close if reader_input!.respond_to?(:close)
141
+ rescue
142
+ Ffmprb.logger.error "ThreadedIoBuffer input closing error: #{$!.message}"
143
+ end
144
+ # reader_done!
145
+ Ffmprb.logger.debug "ThreadedIoBuffer reader terminated (blocks max: #{@stat_blocks_max})"
146
+ end
147
+ end
148
+ end
149
+
150
+ # NOTE writes as much output as possible, then terminates when the reader dies
151
+ def init_writer!
152
+ Thread.new("buffer writer") do
153
+ broken = false
154
+ begin
155
+ while s = @q.deq
156
+ next if broken
157
+ written = 0
158
+ tries = 1
159
+ logged_tries = 1/2
160
+ while !broken
161
+ fail @terminate if @terminate.kind_of?(Exception)
162
+ begin
163
+ output = writer_output!
164
+ written = output.write_nonblock(s) if output # NOTE will only be nil if @terminate is an exception
165
+ break if written == s.length # NOTE kinda optimisation
166
+ s = s[written..-1]
167
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
168
+ if tries == 2 * logged_tries
169
+ Ffmprb.logger.debug "ThreadedIoBuffer writer (to #{output.path}) retrying... (#{tries} writes): #{$!.class}"
170
+ logged_tries = tries
171
+ end
172
+ sleep 0.01
173
+ rescue Errno::EPIPE
174
+ broken = true
175
+ Ffmprb.logger.debug "ThreadedIoBuffer writer (to #{output.path}) broken"
176
+ ensure
177
+ tries += 1
178
+ end
179
+ end
205
180
  end
206
- retry unless @terminate.kind_of?(Exception)
207
181
  ensure
208
- tries += 1
182
+ # terminated!
183
+ begin
184
+ writer_output!.close if !broken && writer_output!.respond_to?(:close)
185
+ rescue
186
+ Ffmprb.logger.error "ThreadedIoBuffer output closing error: #{$!.message}"
187
+ end
188
+ Ffmprb.logger.debug "ThreadedIoBuffer writer terminated (blocks max: #{@stat_blocks_max})"
209
189
  end
210
190
  end
211
191
  end
data/lib/ffmprb/util.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # require 'ffmprb/util/synchro'
2
2
  require 'ffmprb/util/thread'
3
- require 'ffmprb/util/io_buffer'
3
+ require 'ffmprb/util/threaded_io_buffer'
4
4
 
5
5
  require 'open3'
6
6
 
@@ -8,50 +8,81 @@ module Ffmprb
8
8
 
9
9
  module Util
10
10
 
11
+ class TimeLimitError < Error; end
12
+
11
13
  class << self
12
14
 
13
15
  attr_accessor :ffmpeg_cmd, :ffprobe_cmd
16
+ attr_accessor :cmd_timeout
14
17
 
15
- def ffprobe(*args)
16
- sh *ffprobe_cmd, *args
18
+ def ffprobe(*args, limit: nil, timeout: cmd_timeout)
19
+ sh *ffprobe_cmd, *args, limit: limit, timeout: timeout
17
20
  end
18
21
 
19
- def ffmpeg(*args)
22
+ def ffmpeg(*args, limit: nil, timeout: cmd_timeout)
20
23
  args = ['-loglevel', 'debug'] + args if Ffmprb.debug
21
- sh *ffmpeg_cmd, '-y', *args, output: :stderr
24
+ sh *ffmpeg_cmd, '-y', *args, output: :stderr, limit: limit, timeout: timeout
22
25
  end
23
26
 
24
- def sh(*cmd, output: :stdout, log: :stderr)
25
- cmd = cmd.to_a.map(&:to_s)
26
- cmd_str = cmd.join(' ')
27
- Ffmprb.logger.info cmd_str
28
- Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
29
- stdin.close
27
+ def sh(*cmd, output: :stdout, log: :stderr, limit: nil, timeout: cmd_timeout)
28
+ cmd = cmd.map &:to_s unless cmd.size == 1
29
+ cmd_str = cmd.size != 1 ? cmd.map{|c| "\"#{c}\""}.join(' ') : cmd.first
30
+ timeout = [timeout, limit].compact.min
31
+ thr = Thread.new "`#{cmd_str}`" do
32
+ Ffmprb.logger.info "Popening `#{cmd_str}`..."
33
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
34
+ begin
35
+ stdin.close
30
36
 
31
- # XXX process timeouting/cleanup here will be appreciated
37
+ log_cmd = cmd.first.upcase if log
38
+ stdout_r = Reader.new(stdout, output == :stdout, log == :stdout && log_cmd)
39
+ stderr_r = Reader.new(stderr, true, log == :stderr && log_cmd)
32
40
 
33
- begin
34
- log_cmd = "#{(cmd.respond_to?(:first)? cmd : cmd.split(' ')).first.upcase}: " if log
35
- stdout_r = Reader.new(stdout, output == :stdout, log == :stdout && log_cmd)
36
- stderr_r = Reader.new(stderr, true, log == :stderr && log_cmd)
41
+ Thread.timeout_or_live(limit, log: "while waiting for `#{cmd_str}`", timeout: timeout) do |time|
42
+ fail Error, "#{cmd_str}:\n#{stderr_r.read}" unless
43
+ wait_thr.value.exitstatus == 0 # NOTE blocking
44
+ end
45
+ Ffmprb.logger.debug "FINISHED: #{cmd_str}"
37
46
 
38
- raise Error, "#{cmd_str}:\n#{stderr_r.read}" unless
39
- wait_thr.value.exitstatus == 0 # NOTE blocks
47
+ Thread.join_children! limit, timeout: timeout
40
48
 
41
- # NOTE only one of them will return non-nil, see above
42
- stdout_r.read || stderr_r.read
43
- ensure
44
- begin
45
- stdout_r.join if stdout_r
46
- stdout_r = nil
47
- stderr_r.join if stderr_r
48
- rescue
49
- Ffmprb.logger.error "Thread joining error: #{$!.message}"
50
- stderr_r.join if stdout_r
49
+ # NOTE only one of them will return non-nil, see above
50
+ stdout_r.read || stderr_r.read
51
+ ensure
52
+ process_dead! wait_thr, cmd_str, limit
51
53
  end
52
- Ffmprb.logger.debug "FINISHED: #{cmd_str}"
53
54
  end
54
55
  end
56
+ thr.value
57
+ end
58
+
59
+ protected
60
+
61
+ def process_dead!(wait_thr, cmd_str, limit)
62
+ grace = limit ? limit/4 : 1
63
+ return unless wait_thr.alive?
64
+
65
+ # NOTE a simplistic attempt to gracefully terminate a child process
66
+ # the successful completion is via exception...
67
+ begin
68
+ Ffmprb.logger.info "Sorry it came to this, but I'm terminating `#{cmd_str}`(#{wait_thr.pid})..."
69
+ ::Process.kill 'TERM', wait_thr.pid
70
+ sleep grace
71
+ Ffmprb.logger.info "Very sorry it came to this, but I'm terminating `#{cmd_str}`(#{wait_thr.pid}) again..."
72
+ ::Process.kill 'TERM', wait_thr.pid
73
+ sleep grace
74
+ Ffmprb.logger.warn "Die `#{cmd_str}`(#{wait_thr.pid}), die!.. (killing amok)"
75
+ ::Process.kill 'KILL', wait_thr.pid
76
+ sleep grace
77
+ Ffmprb.logger.warn "Checking if `#{cmd_str}`(#{wait_thr.pid}) finally dead..."
78
+ ::Process.kill 0, wait_thr.pid
79
+ Ffmprb.logger.error "Still alive -- `#{cmd_str}`(#{wait_thr.pid}), giving up..."
80
+ rescue Errno::ESRCH
81
+ Ffmprb.logger.info "Apparently `#{cmd_str}`(#{wait_thr.pid}) is dead..."
82
+ end
83
+
84
+ fail Error, "System error or something: waiting for the thread running `#{cmd_str}`(#{wait_thr.pid})..." unless
85
+ wait_thr.join limit
55
86
  end
56
87
 
57
88
  end
@@ -65,7 +96,7 @@ module Ffmprb
65
96
  super "reader" do
66
97
  begin
67
98
  while s = input.gets
68
- Ffmprb.logger.debug log + s.chomp if log
99
+ Ffmprb.logger.debug "#{log}: #{s.chomp}" if log
69
100
  @output << s if store
70
101
  end
71
102
  @queue.enq @output
@@ -78,7 +109,7 @@ module Ffmprb
78
109
  def read
79
110
  case res = @queue.deq
80
111
  when Exception
81
- raise res
112
+ fail res
82
113
  when ''
83
114
  nil
84
115
  else
@@ -1,3 +1,3 @@
1
1
  module Ffmprb
2
- VERSION = "0.7.0"
2
+ VERSION = '0.7.3'
3
3
  end
data/lib/ffmprb.rb CHANGED
@@ -1,41 +1,23 @@
1
- require 'ffmprb/execution'
2
- require 'ffmprb/file'
3
- require 'ffmprb/filter'
4
- require 'ffmprb/find_silence'
5
- require 'ffmprb/process'
6
- require 'ffmprb/util'
7
- require 'ffmprb/version'
8
-
9
1
  require 'logger'
10
2
 
3
+ # IMPORTANT NOTE ffmprb uses threads internally, however, it is not "thread-safe"
4
+
11
5
  module Ffmprb
12
6
 
13
7
  ENV_VAR_FALSE_REGEX = /^(0|no?|false)?$/i
14
8
 
9
+ CGA = '320x200'
15
10
  QVGA = '320x240'
16
11
  HD_720p = '1280x720'
17
12
  HD_1080p = '1920x1080'
18
13
 
19
- class Error < StandardError
20
- end
21
-
22
- Util.ffmpeg_cmd = ['ffmpeg']
23
- Util.ffprobe_cmd = ['ffprobe']
24
-
25
- Process.duck_audio_hi = 0.9
26
- Process.duck_audio_lo = 0.1
27
- Process.duck_audio_transition_sec = 1
28
- Process.duck_audio_silent_min_sec = 3
29
- Filter.silence_noise_max_db = -40
30
-
31
- Util::IoBuffer.blocks_max = 1024
32
- Util::IoBuffer.block_size = 64*1024
33
- Util::IoBuffer.timeout = 9
14
+ class Error < StandardError; end
34
15
 
35
16
  class << self
36
17
 
37
18
  # NOTE the form with the block returns the result of #run
38
- # NOTE the form without the block returns the process (before it is run)
19
+ # NOTE the form without the block returns the process (before it is run) - advanced use
20
+ # XXX is this clear enough? Do we really need the second form?
39
21
  def process(*args, &blk)
40
22
  logger.debug "Starting process with #{args} in #{blk.source_location}"
41
23
  process = Process.new
@@ -68,3 +50,7 @@ module Ffmprb
68
50
  end
69
51
 
70
52
  Ffmprb.debug = ENV.fetch('FFMPRB_DEBUG', '') !~ Ffmprb::ENV_VAR_FALSE_REGEX
53
+
54
+ Dir["#{__FILE__.slice /(.*).rb$/, 1}/**/*.rb"].each{|f| require f} # XXX require_sub __FILE__ # or something
55
+
56
+ require 'defaults'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ffmprb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - showbox.com
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2015-07-28 00:00:00.000000000 Z
12
+ date: 2015-10-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mkfifo
@@ -147,18 +147,20 @@ files:
147
147
  - circle.yml
148
148
  - exe/ffmprb
149
149
  - ffmprb.gemspec
150
+ - lib/defaults.rb
150
151
  - lib/ffmprb.rb
151
152
  - lib/ffmprb/execution.rb
152
153
  - lib/ffmprb/file.rb
154
+ - lib/ffmprb/file/sample.rb
153
155
  - lib/ffmprb/filter.rb
154
156
  - lib/ffmprb/find_silence.rb
155
157
  - lib/ffmprb/process.rb
156
158
  - lib/ffmprb/process/input.rb
157
159
  - lib/ffmprb/process/output.rb
158
160
  - lib/ffmprb/util.rb
159
- - lib/ffmprb/util/io_buffer.rb
160
161
  - lib/ffmprb/util/synchro.rb
161
162
  - lib/ffmprb/util/thread.rb
163
+ - lib/ffmprb/util/threaded_io_buffer.rb
162
164
  - lib/ffmprb/version.rb
163
165
  homepage: https://github.com/showbox-oss/ffmprb
164
166
  licenses: []