ffmprb 0.7.0 → 0.7.3

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.
@@ -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: []