ffmprb 0.10.1 → 0.11.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Guardfile +1 -1
- data/README.md +160 -67
- data/exe/ffmprb +1 -1
- data/ffmprb.gemspec +6 -4
- data/lib/defaults.rb +2 -0
- data/lib/ffmprb.rb +2 -2
- data/lib/ffmprb/execution.rb +29 -13
- data/lib/ffmprb/process.rb +5 -3
- data/lib/ffmprb/process/input.rb +9 -3
- data/lib/ffmprb/process/input/looping.rb +9 -14
- data/lib/ffmprb/process/output.rb +8 -2
- data/lib/ffmprb/util.rb +10 -2
- data/lib/ffmprb/util/proc_vis.rb +1 -0
- data/lib/ffmprb/util/threaded_io_buffer.rb +72 -61
- data/lib/ffmprb/version.rb +4 -1
- metadata +22 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 428b5ce07b48c960ffe05fded9e45d6f458e405b
|
4
|
+
data.tar.gz: 686389df8fd80d665105cdd12682f5f547ec48ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1addc97d33e9e6799081b19617a8f24c00f1f37d6ebeec5dca87893201090a4e2d2a14be72f50d1e80457ff88e7e655befd59cb3f3cda3184e1febdb51ddb980
|
7
|
+
data.tar.gz: 943f57a80800d690cb742c0264668dca0a7816cef88d222cacd5a03daeb0d9e81071bb6bcd6fbaa4df0b886a5d3a20707d25b57a96b44cef035121959d8b83e8
|
data/Guardfile
CHANGED
@@ -6,7 +6,7 @@ guard :rspec,
|
|
6
6
|
:failed_mode => :focus do
|
7
7
|
|
8
8
|
watch(%r{^spec/.+_spec\.rb$})
|
9
|
-
watch(%r{^lib/(
|
9
|
+
watch(%r{^lib/([^/]+).+\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
10
10
|
watch('spec/spec_helper.rb') { 'spec' }
|
11
11
|
watch('.rspec') { 'spec' }
|
12
12
|
end
|
data/README.md
CHANGED
@@ -3,30 +3,31 @@
|
|
3
3
|
[![Circle CI](https://circleci.com/gh/showbox-oss/ffmprb.svg?style=svg)](https://circleci.com/gh/showbox-oss/ffmprb)
|
4
4
|
## your audio/video montage pal, based on [ffmpeg](https://ffmpeg.org)
|
5
5
|
|
6
|
-
A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends
|
6
|
+
A video and audio composing DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends (with CLI)
|
7
7
|
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
Ffmprb.process do
|
8
|
+
If you're neither a video technologist, nor a video artist, you neither need cumbersome low level tools (like ffmpeg), nor heavy and costly high level tools (like Premiere).
|
11
9
|
|
12
|
-
|
13
|
-
in_sound = input('track.mp3')
|
14
|
-
output 'cine.flv', video: {resolution: '1280x720'} do
|
15
|
-
roll in_main.crop(0.25).cut(from: 2, to: 5), transition: {blend: 1}
|
16
|
-
roll in_main.volume(2).cut(from: 6, to: 16), after: 2, transition: {blend: 1}
|
17
|
-
overlay in_sound.volume(0.8)
|
18
|
-
end
|
10
|
+
Any script-able person can manipulate video/audio media -- or automate processing thereof -- with ffmprb.
|
19
11
|
|
12
|
+
Allows for scripts like
|
13
|
+
```ruby
|
14
|
+
in_main = input('flick.mp4')
|
15
|
+
output 'cine.flv', video: {resolution: '1280x720'} do
|
16
|
+
roll in_main.crop(0.25).cut(from: 2, to: 5), transition: {blend: 1}
|
17
|
+
roll in_main.volume(2).cut(from: 6, to: 16), after: 2, transition: {blend: 1}
|
18
|
+
overlay input('track.mp3').volume(0.8)
|
20
19
|
end
|
21
20
|
```
|
22
21
|
and saves you from the horror of...
|
23
22
|
```
|
24
|
-
ffmpeg -y -noautorotate -i flick.mp4 -i track.
|
23
|
+
ffmpeg -y -noautorotate -i flick.mp4 -i track.mp3 -filter_complex "[0:v] fps=fps=16 [cptmo0rl0:v]; [0:a] anull [cptmo0rl0:a]; [cptmo0rl0:v] crop=x=in_w*0.25:y=in_h*0.25:w=in_w*0.5:h=in_h*0.5 [tmptmo0rl0:v]; [tmptmo0rl0:v] scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih), setsar=1, pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2, setsar=1 [tmo0rl0:v]; [cptmo0rl0:a] anull [tmo0rl0:a]; color=0x000000@0:d=3:s=1280x720:r=16 [blo0rl0:v]; [tmo0rl0:v] [blo0rl0:v] concat=2:v=1:a=0 [pdo0rl0:v]; [pdo0rl0:v] trim=2:5, setpts=PTS-STARTPTS [o0rl0:v]; aevalsrc=0:d=3 [blo0rl0:a]; [tmo0rl0:a] [blo0rl0:a] concat=2:v=0:a=1 [pdo0rl0:a]; [pdo0rl0:a] atrim=2:5, asetpts=PTS-STARTPTS [o0rl0:a]; color=0x000000@0:d=1.0:s=1280x720:r=16 [bl0:v]; aevalsrc=0:d=1.0 [bl0:a]; [bl0:v] trim=0:1.0, setpts=PTS-STARTPTS [o0tm0b:v]; [bl0:a] atrim=0:1.0, asetpts=PTS-STARTPTS [o0tm0b:a]; color=0xFFFFFF@1:d=1.0:s=1280x720:r=16 [blndo0tm0b:v]; [o0tm0b:v] [blndo0tm0b:v] alphamerge, fade=out:d=1.0:alpha=1 [xblndo0tm0b:v]; [o0rl0:v] [xblndo0tm0b:v] overlay=x=0:y=0:eof_action=pass [o0tn0:v]; [o0tm0b:a] afade=out:d=1.0:curve=hsin [blndo0tm0b:a]; [o0rl0:a] afade=in:d=1.0:curve=hsin [xblndo0tm0b:a]; [blndo0tm0b:a] apad [apdblndo0tm0b:a]; [xblndo0tm0b:a] [apdblndo0tm0b:a] amix=2:duration=shortest:dropout_transition=0, volume=2 [o0tn0:a]; [0:v] scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih), setsar=1, pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2, setsar=1, fps=fps=16 [ldtmo0rl1:v]; [0:a] anull [ldtmo0rl1:a]; [ldtmo0rl1:v] copy [tmo0rl1:v]; [ldtmo0rl1:a] volume='2':eval=frame [tmo0rl1:a]; color=0x000000@0:d=10:s=1280x720:r=16 [blo0rl1:v]; [tmo0rl1:v] [blo0rl1:v] concat=2:v=1:a=0 [pdo0rl1:v]; [pdo0rl1:v] trim=6:16, setpts=PTS-STARTPTS [o0rl1:v]; aevalsrc=0:d=10 [blo0rl1:a]; [tmo0rl1:a] [blo0rl1:a] concat=2:v=0:a=1 [pdo0rl1:a]; [pdo0rl1:a] atrim=6:16, asetpts=PTS-STARTPTS [o0rl1:a]; color=0x000000@0:d=3.0:s=1280x720:r=16 [blo0tn01:v]; aevalsrc=0:d=3.0 [blo0tn01:a]; [o0tn0:v] [blo0tn01:v] concat=2:v=1:a=0 [pdo0tn01:v]; [o0tn0:a] [blo0tn01:a] concat=2:v=0:a=1 [pdo0tn01:a]; [pdo0tn01:v] split [pdo0tn01a:v] [pdo0tn01b:v]; [pdo0tn01:a] asplit [pdo0tn01a:a] [pdo0tn01b:a]; [pdo0tn01a:v] trim=0:2, setpts=PTS-STARTPTS [tmo0tn01a:v]; [pdo0tn01a:a] atrim=0:2, asetpts=PTS-STARTPTS [tmo0tn01a:a]; [pdo0tn01b:v] trim=2:3.0, setpts=PTS-STARTPTS [o0tm1b:v]; [pdo0tn01b:a] atrim=2:3.0, asetpts=PTS-STARTPTS [o0tm1b:a]; color=0xFFFFFF@1:d=1.0:s=1280x720:r=16 [blndo0tm1b:v]; [o0tm1b:v] [blndo0tm1b:v] alphamerge, fade=out:d=1.0:alpha=1 [xblndo0tm1b:v]; [o0rl1:v] [xblndo0tm1b:v] overlay=x=0:y=0:eof_action=pass [o0tn1:v]; [o0tm1b:a] afade=out:d=1.0:curve=hsin [blndo0tm1b:a]; [o0rl1:a] afade=in:d=1.0:curve=hsin [xblndo0tm1b:a]; [blndo0tm1b:a] apad [apdblndo0tm1b:a]; [xblndo0tm1b:a] [apdblndo0tm1b:a] amix=2:duration=shortest:dropout_transition=0, volume=2 [o0tn1:a]; [tmo0tn01a:v] [o0tn1:v] concat=2:v=1:a=0 [o0o:v]; [tmo0tn01a:a] [o0tn1:a] concat=2:v=0:a=1 [o0o:a]; [1:a] anull [ldo0l0:a]; [ldo0l0:a] volume='0.8':eval=frame [o0l0:a]; [o0o:v] copy [o0o0:v]; [o0l0:a] apad [apdo0l0:a]; [o0o:a] [apdo0l0:a] amix=2:duration=shortest:dropout_transition=0, volume=2 [o0o0:a]" -map "[o0o0:v]" -map "[o0o0:a]" -c:a libmp3lame cine.flv
|
25
24
|
```
|
26
25
|
...that's the idea, but there's much more to it.
|
26
|
+
|
27
27
|
The docs, as well as any other part of this gem, are a work in progress.
|
28
28
|
So you're very welcome to look around the [specs](https://github.com/showbox-oss/ffmprb/tree/master/spec) for the actual functionality coverage.
|
29
29
|
|
30
|
+
|
30
31
|
## Installation
|
31
32
|
|
32
33
|
Add this line to your application's Gemfile:
|
@@ -43,105 +44,197 @@ Or install it yourself as:
|
|
43
44
|
|
44
45
|
$ gem install ffmprb
|
45
46
|
|
47
|
+
|
46
48
|
## DSL & Usage
|
47
49
|
|
48
50
|
The DSL strives to provide for the most common script cases in the most natural way:
|
49
51
|
you just describe what should be shown -- in an action sequence, like the following.
|
50
52
|
|
51
|
-
Play your _episode_ teaser snippet:
|
52
53
|
```ruby
|
54
|
+
# Play your _episode_ teaser snippet:
|
53
55
|
lay episode.cut(to: 60), transition: {blend: 3}
|
54
|
-
|
55
|
-
Overlay anything after that with your channel _logo_:
|
56
|
-
```ruby
|
56
|
+
|
57
|
+
# Overlay anything after that with your channel _logo_:
|
57
58
|
overlay logo.loop.cut(to: 33), after: 3, transition: {blend: 1} # both ways
|
58
|
-
|
59
|
-
Start with rolling some _intro_ flick:
|
60
|
-
```ruby
|
59
|
+
|
60
|
+
# Start with rolling some _intro_ flick:
|
61
61
|
lay intro, transition: {blend: 1}
|
62
|
-
|
63
|
-
Overlay it with some special _badge_ sprite:
|
64
|
-
```ruby
|
62
|
+
|
63
|
+
# Overlay it with some special _badge_ sprite:
|
65
64
|
overlay badge.loop, at: 1, transition: {burn: 1}
|
66
|
-
|
67
|
-
Show _title_:
|
68
|
-
```ruby
|
65
|
+
|
66
|
+
# Show _title_:
|
69
67
|
lay title, transition: {blend: 2}
|
70
|
-
|
71
|
-
Play some of your _episode_:
|
72
|
-
```ruby
|
68
|
+
|
69
|
+
# Play some of your _episode_:
|
73
70
|
lay episode.cut(from: 60, to: 540)
|
74
|
-
|
75
|
-
Oh well, roll some _promo_ material:
|
76
|
-
```ruby
|
71
|
+
|
72
|
+
# Oh well, roll some _promo_ material:
|
77
73
|
lay promo, transition: {pixel: 2}
|
78
|
-
|
79
|
-
Play most of your _episode_:
|
80
|
-
```ruby
|
74
|
+
|
75
|
+
# Play most of your _episode_:
|
81
76
|
lay episode.cut(from: 540, to: 1080)
|
82
|
-
|
83
|
-
Roll the _credits_:
|
84
|
-
```ruby
|
77
|
+
|
78
|
+
# Roll the _credits_:
|
85
79
|
overlay credits, at: 1075
|
80
|
+
|
81
|
+
# Finish by playing your special _outro_:
|
82
|
+
lay outro, transition: {blend: 1}
|
83
|
+
|
84
|
+
# Fin
|
86
85
|
```
|
87
|
-
|
86
|
+
|
87
|
+
### In the code
|
88
|
+
|
89
|
+
The block above is to be given to an `Ffmprb.process` call:
|
90
|
+
|
88
91
|
```ruby
|
89
|
-
|
92
|
+
Ffmprb.process do
|
93
|
+
|
94
|
+
# Play your _episode_ teaser snippet:
|
95
|
+
lay episode.cut(to: 60), transition: {blend: 3}
|
96
|
+
|
97
|
+
...
|
98
|
+
|
99
|
+
end
|
90
100
|
```
|
91
101
|
|
92
|
-
|
102
|
+
The block runs in the context of a new `Ffmprb::Process`, so any instance data shall be passed by value as follows:
|
103
|
+
|
93
104
|
```ruby
|
94
|
-
|
95
|
-
|
96
|
-
|
105
|
+
Ffmprb.process @episode, @teaser_length do
|
106
|
+
|episode, teaser_length|
|
107
|
+
|
108
|
+
# Play your _episode_ teaser snippet:
|
109
|
+
lay episode.cut(to: teaser_length), transition: {blend: 3}
|
110
|
+
|
111
|
+
...
|
112
|
+
|
97
113
|
end
|
98
114
|
```
|
99
|
-
|
100
|
-
|
115
|
+
|
116
|
+
### Command line
|
117
|
+
|
118
|
+
The `ffmprb` command-line utility expects a script on its standard input:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
# episode_01.ffmprb
|
122
|
+
|
123
|
+
# Play your _episode_ teaser snippet:
|
124
|
+
lay input('episode_01.mov').cut(to: 60), transition: {blend: 3}
|
125
|
+
|
126
|
+
...
|
127
|
+
```
|
128
|
+
|
129
|
+
$ ffmprb < episode_01.ffmprb
|
130
|
+
|
131
|
+
|
132
|
+
And it can take parameters for the sake of automation convenience:
|
133
|
+
|
101
134
|
```ruby
|
102
|
-
#
|
135
|
+
# episode_make.ffmprb
|
103
136
|
|episode, logo, intro, badge, title, promo, credits, outro|
|
104
137
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
lay title, transition: {blend: 2}
|
110
|
-
lay episode.cut(from: 60, to: 540)
|
111
|
-
lay promo, transition: {pixel: 2}
|
112
|
-
lay episode.cut(from: 540, to: 1080)
|
113
|
-
overlay credits, at: 535
|
114
|
-
lay outro, transition: {blend: 1}
|
138
|
+
# Play your _episode_ teaser snippet:
|
139
|
+
lay input(episode).cut(to: 60), transition: {blend: 3}
|
140
|
+
|
141
|
+
...
|
115
142
|
```
|
116
143
|
|
117
|
-
|
144
|
+
$ ffmprb ep01raw.mov logo.png intro.avi new_new.gif ep01tit.mov showbox_promo.mp4 ep01creds.avi ep01out.mov < episode_make.ffmprb
|
118
145
|
|
119
|
-
- Ffmprb is a work in progress, and even more so than Ffmpeg itself;
|
120
|
-
use at your own risk and check thoroughly for production fitness in your project.
|
121
|
-
- Ffmprb uses threads internally, however, it is not thread-safe interface-wise:
|
122
|
-
you must not share its objects between different threads.
|
123
146
|
|
147
|
+
### The defaults
|
124
148
|
|
149
|
+
The defaults [defaults](https://github.com/showbox-oss/ffmprb/tree/master/lib/defaults.rb) are provided for every possible configuration option (optional options' defaults for the methods below in particular), you're welcome to config anything in your ffmprb scripts.
|
125
150
|
|
126
|
-
### General structure
|
127
151
|
|
128
|
-
|
152
|
+
### Advanced usage
|
153
|
+
|
154
|
+
Anything ruby-valid will work -- the script may be generated on the fly:
|
155
|
+
```ruby
|
156
|
+
transitions = [:blend, :burn, :zoom]
|
157
|
+
photos.shuffle.each do |photo|
|
158
|
+
lay photo.loop.cut(to: rand * 3), transition: {transitions.shuffle.first => 1}
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
### Inputs/outputs
|
163
|
+
|
164
|
+
Inside a `process` block, there are `input` definitions and `output` definitions;
|
129
165
|
naturally, the latter use the former:
|
130
166
|
```ruby
|
131
|
-
Ffmprb.process do
|
167
|
+
Ffmprb.process do
|
132
168
|
|
133
|
-
in_main = input(
|
134
|
-
output(
|
135
|
-
|
169
|
+
in_main = input(av_input1)
|
170
|
+
output(av_output1, video: {resolution: Ffmprb::HD_720p, fps: 25}) do
|
171
|
+
lay in_main.crop(0.05), transition: {blend: 1}
|
136
172
|
end
|
137
173
|
|
138
174
|
end
|
139
175
|
```
|
140
176
|
|
177
|
+
`input`(_file_, [video: {false | {[auto_rotate:], [fps:]}}], [audio: false])
|
178
|
+
|
179
|
+
`input` returns a _reel_.
|
180
|
+
|
181
|
+
`output`(_file_, [video: {false | {[resolution:], [fps:]}}], [audio: {false | {[encoder:], [sampling_freq:]}}])
|
182
|
+
|
183
|
+
`output` also takes a block where you get to use `lay` and `overlay` methods:
|
184
|
+
|
185
|
+
`lay`(_reel_[, after: _sec_[, transition: {blend: _sec_}])
|
186
|
+
|
187
|
+
`lay` renders the reel full screen after the previously `lay`ed reel.
|
188
|
+
|
189
|
+
`overlay`(_reel_[, at: _sec_][, duck: :audio])
|
190
|
+
|
191
|
+
`overlay` is currently functional just for audio reels, sorry.
|
192
|
+
|
193
|
+
### Available reel modifier (filter) methods
|
194
|
+
|
195
|
+
`crop`({_ratio_ | {[top: _ratio_][, left: _ratio_][, bottom: _ratio_][, right: _ratio_][, width: _ratio_][, height: _ratio_]}})
|
196
|
+
|
197
|
+
`crop` crops the reel frames (e.g. `in1.crop(0.1)` will remove 1/10th of the frame from each side)
|
198
|
+
|
199
|
+
`cut`([from: _sec_][, to: _sec_])
|
200
|
+
|
201
|
+
`cut` cuts the reel from `from:` to `to:`.
|
202
|
+
|
203
|
+
`loop`([_times_])
|
204
|
+
|
205
|
+
`loop` loops(!) the reel so many times (no _times_ param means maximum times currently possible).
|
206
|
+
|
207
|
+
`mute`
|
208
|
+
|
209
|
+
`volume`(_ratio_)
|
210
|
+
|
211
|
+
`volume` changes the volume proportionally to the source. `mute` mutes.
|
212
|
+
|
213
|
+
`video`
|
214
|
+
|
215
|
+
`video` channels just the video from the reel.
|
216
|
+
|
217
|
+
`audio`
|
218
|
+
|
219
|
+
`audio` channels just the audio from the reel.
|
220
|
+
|
221
|
+
`copy`(_reel_)
|
222
|
+
|
223
|
+
`copy` copies the reel's modifier chain _onto_ the given reel.
|
224
|
+
|
225
|
+
|
226
|
+
### Attention
|
227
|
+
|
228
|
+
- Ffmprb is a work in progress, and even more so than Ffmpeg itself;
|
229
|
+
use at your own risk and check thoroughly for production fitness in your project.
|
230
|
+
- Ffmprb uses threads internally, however, it is not thread-safe interface-wise:
|
231
|
+
you must not share its objects between different threads.
|
232
|
+
|
141
233
|
### ProcVis support (experimental)
|
142
234
|
|
143
235
|
To enable [ProcVis](https://procvis.io) support (source), define `FFMPRB_PROC_VIS_FIREBASE_URL=my-proc-vis-io` (replace with your Firebase instance) in your running environment and watch the log for `You may view your process visualised at: https://proc-vis-io.firebaseapp.com/?pid=70311657638000 (a sample ProcVis snapshot of a full specs run).
|
144
236
|
|
237
|
+
|
145
238
|
## Development
|
146
239
|
|
147
240
|
After checking out the repo, run `bin/setup` to install dependencies.
|
data/exe/ffmprb
CHANGED
data/ffmprb.gemspec
CHANGED
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ['costa@mouldwarp.com']
|
11
11
|
|
12
12
|
spec.summary = "ffmprb is your audio/video montage friend, based on https://ffmpeg.org"
|
13
|
-
spec.description = "A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends"
|
14
|
-
spec.homepage =
|
13
|
+
spec.description = "A video and audio composing DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends"
|
14
|
+
spec.homepage = Ffmprb::GEM_GITHUB_URL
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
17
|
spec.bindir = 'exe'
|
@@ -20,10 +20,12 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
# NOTE I'm not happy with this dependency, and there's nothing crossplatform (= for windoze too) at the moment
|
22
22
|
spec.add_dependency 'mkfifo', '~> 0.1.1'
|
23
|
+
# NOTE make it into an optional dependency? Nah for now
|
24
|
+
spec.add_dependency 'thor', '~> 0.19.1'
|
23
25
|
|
24
26
|
spec.add_development_dependency 'bundler', '>= 1.11.2'
|
25
|
-
spec.add_development_dependency 'byebug', '>= 8.2.
|
26
|
-
spec.add_development_dependency 'simplecov', '>= 0.
|
27
|
+
spec.add_development_dependency 'byebug', '>= 8.2.4'
|
28
|
+
spec.add_development_dependency 'simplecov', '>= 0.11.2'
|
27
29
|
spec.add_development_dependency 'guard-rspec', '>= 4.6.5'
|
28
30
|
spec.add_development_dependency 'guard-bundler', '>= 2.1.0'
|
29
31
|
spec.add_development_dependency 'rake', '>= 11.1.2'
|
data/lib/defaults.rb
CHANGED
@@ -21,6 +21,7 @@ module Ffmprb
|
|
21
21
|
Process.output_video_resolution = CGA
|
22
22
|
Process.output_video_fps = 16
|
23
23
|
Process.output_audio_encoder = 'libmp3lame'
|
24
|
+
Process.output_audio_sampling_freq = nil # NOTE Use ffmpeg default by default, specify otherwise e.g. 44100
|
24
25
|
|
25
26
|
Util.cmd_timeout = 30
|
26
27
|
Util.ffmpeg_cmd = %w[ffmpeg -y]
|
@@ -39,6 +40,7 @@ module Ffmprb
|
|
39
40
|
|
40
41
|
# NOTE http://12factor.net etc
|
41
42
|
|
43
|
+
Ffmprb.log_level = Logger::INFO
|
42
44
|
Ffmprb.ffmpeg_debug = ENV.fetch('FFMPRB_FFMPEG_DEBUG', '') !~ Ffmprb::ENV_VAR_FALSE_REGEX
|
43
45
|
Ffmprb.debug = ENV.fetch('FFMPRB_DEBUG', '') !~ Ffmprb::ENV_VAR_FALSE_REGEX
|
44
46
|
|
data/lib/ffmprb.rb
CHANGED
@@ -35,11 +35,11 @@ module Ffmprb
|
|
35
35
|
end
|
36
36
|
alias :action! :process # ;)
|
37
37
|
|
38
|
-
attr_accessor :debug, :ffmpeg_debug
|
38
|
+
attr_accessor :debug, :ffmpeg_debug, :log_level
|
39
39
|
|
40
40
|
def logger
|
41
41
|
@logger ||= Logger.new(STDERR).tap do |logger|
|
42
|
-
logger.level = debug ? Logger::DEBUG :
|
42
|
+
logger.level = debug ? Logger::DEBUG : Ffmprb.log_level
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
data/lib/ffmprb/execution.rb
CHANGED
@@ -1,23 +1,39 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
1
3
|
module Ffmprb
|
2
4
|
|
3
|
-
class Execution
|
5
|
+
class Execution < Thor
|
4
6
|
|
5
|
-
def
|
6
|
-
@params = params
|
7
|
-
@script = eval("lambda{#{script}}")
|
8
|
-
end
|
7
|
+
def self.exit_on_failure?; true; end
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
class_option :debug, :type => :boolean, :default => false
|
10
|
+
class_option :verbose, :aliases => '-v', :type => :boolean, :default => false
|
11
|
+
class_option :quiet, :aliases => '-q', :type => :boolean, :default => false
|
13
12
|
|
14
|
-
|
13
|
+
default_task :process
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
desc :process, "Reads an ffmprb script from STDIN and carries it out. See #{GEM_GITHUB_URL}"
|
16
|
+
def process(*ios)
|
17
|
+
script = eval("lambda{#{STDIN.read}}")
|
18
|
+
Ffmprb.log_level =
|
19
|
+
if options[:debug]
|
20
|
+
Logger::DEBUG
|
21
|
+
elsif options[:verbose]
|
22
|
+
Logger::INFO
|
23
|
+
elsif options[:quiet]
|
24
|
+
Logger::ERROR
|
25
|
+
else
|
26
|
+
Logger::WARN
|
27
|
+
end
|
28
|
+
Ffmprb.process *ios, ignore_broken_pipes: false, &script
|
29
|
+
end
|
30
|
+
|
31
|
+
# NOTE a hack from http://stackoverflow.com/a/23955971/714287
|
32
|
+
def method_missing(method, *args)
|
33
|
+
args = [:process, method.to_s] + args
|
34
|
+
self.class.start(args)
|
35
|
+
end
|
19
36
|
|
20
|
-
Execution.new(*ARGV, script: STDIN.read).run
|
21
37
|
end
|
22
38
|
|
23
39
|
end
|
data/lib/ffmprb/process.rb
CHANGED
@@ -16,13 +16,14 @@ module Ffmprb
|
|
16
16
|
attr_accessor :output_video_resolution
|
17
17
|
attr_accessor :output_video_fps
|
18
18
|
attr_accessor :output_audio_encoder
|
19
|
+
attr_accessor :output_audio_sampling_freq
|
19
20
|
|
20
21
|
attr_accessor :timeout
|
21
22
|
|
22
23
|
def intermediate_channel_extname(video:, audio:)
|
23
24
|
if video
|
24
25
|
if audio
|
25
|
-
'.flv'
|
26
|
+
'.flv' # TODO optimise this by using http://superuser.com/a/522853 or something
|
26
27
|
else
|
27
28
|
'.y4m'
|
28
29
|
end
|
@@ -53,7 +54,8 @@ module Ffmprb
|
|
53
54
|
end
|
54
55
|
def output_audio_options
|
55
56
|
{
|
56
|
-
encoder: output_audio_encoder
|
57
|
+
encoder: output_audio_encoder,
|
58
|
+
sampling_freq: output_audio_sampling_freq
|
57
59
|
}
|
58
60
|
end
|
59
61
|
|
@@ -114,7 +116,7 @@ module Ffmprb
|
|
114
116
|
@parent = opts.delete(:parent)
|
115
117
|
parent.proc_vis_node self if parent
|
116
118
|
self.ignore_broken_pipes = opts.delete(:ignore_broken_pipes)
|
117
|
-
|
119
|
+
Util.assert_options_empty! opts
|
118
120
|
@inputs, @outputs = [], []
|
119
121
|
end
|
120
122
|
|
data/lib/ffmprb/process/input.rb
CHANGED
@@ -22,14 +22,14 @@ module Ffmprb
|
|
22
22
|
fps = nil # NOTE ah, ruby
|
23
23
|
args.concat %W[-noautorotate] unless video.delete(:auto_rotate)
|
24
24
|
args.concat %W[-r #{fps}] if (fps = video.delete(:fps))
|
25
|
-
|
25
|
+
Util.assert_options_empty! video
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
def audio_args(audio=nil)
|
30
30
|
audio = Process.input_audio_options.merge(audio.to_h)
|
31
31
|
[].tap do |args|
|
32
|
-
|
32
|
+
Util.assert_options_empty! audio
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -74,8 +74,14 @@ module Ffmprb
|
|
74
74
|
else
|
75
75
|
Filter.copy "#{in_lbl}:v", "#{lbl}:v"
|
76
76
|
end
|
77
|
+
elsif video
|
78
|
+
fail Error, "No video stream to provide"
|
77
79
|
end),
|
78
|
-
*(audio && channel?(:audio)
|
80
|
+
*(if audio && channel?(:audio)
|
81
|
+
Filter.anull "#{in_lbl}:a", "#{lbl}:a"
|
82
|
+
elsif audio
|
83
|
+
fail Error, "No audio stream to provide"
|
84
|
+
end)
|
79
85
|
]
|
80
86
|
end
|
81
87
|
|
@@ -30,16 +30,12 @@ module Ffmprb
|
|
30
30
|
# 2) Tee+buffer the original raw input io: one stream goes back into the process throw the raw input io replacement fifo; the other is fed into the filtering process
|
31
31
|
# 3) Which uses the same underlying filters to produce a filtered and parameterised stream, which is fed into the looping process through a N-Tee+buffer
|
32
32
|
# 4) Invoke the looping process which just concatenates its N inputs and produces the new raw input (the aux input)
|
33
|
-
# XXX
|
34
|
-
# -) If the consumer is broken of the:
|
35
|
-
# a. raw input - the Tee+buffer is resilient - unless the f-p-l breaks too;
|
36
|
-
# b. the f-p-l stream - the looping process fails, the N-Tee+buffer breaks, the filtering process fails, and the Tee+buffer may fail
|
37
33
|
|
38
34
|
# Looping
|
39
35
|
# NOTE all the processing is done before looping
|
40
36
|
|
41
37
|
aux_input(video: video, audio: audio).filters_for lbl,
|
42
|
-
video: OpenStruct.new, audio: OpenStruct.new
|
38
|
+
video: video && OpenStruct.new, audio: audio && OpenStruct.new
|
43
39
|
end
|
44
40
|
|
45
41
|
protected
|
@@ -91,12 +87,11 @@ module Ffmprb
|
|
91
87
|
|
92
88
|
buff_ios = (1..times).map{File.temp_fifo intermediate_extname}
|
93
89
|
Ffmprb.logger.debug "Preprocessed #{dst_io.path} will be teed to #{buff_ios.map(&:path).join '; '}"
|
94
|
-
looping = true
|
95
90
|
Util::Thread.new "cloning buffer watcher" do
|
96
|
-
dst_io.threaded_buffered_copy_to
|
97
|
-
|
98
|
-
|
99
|
-
|
91
|
+
dst_io.threaded_buffered_copy_to(*buff_ios).tap do |io_buff|
|
92
|
+
Util::Thread.join_children!
|
93
|
+
Ffmprb.logger.warn "Looping ~from #{src_io.path} finished before its consumer: if you just wanted to loop input #{Util.ffmpeg_inputs_max} times, that's fine, but if you expected it to loop indefinitely... #{Util.ffmpeg_inputs_max} is the maximum #loop can do at the moment, and it may just not be enough in this case (workaround by concatting or file a complaint at #{Ffmprb::GEM_GITHUB_URL}/issues please)." if times == Util.ffmpeg_inputs_max && io_buff.stats.blocks_buff == 0
|
94
|
+
end
|
100
95
|
end
|
101
96
|
|
102
97
|
# Ffmprb.logger.debug "Concatenation of #{buff_ios.map(&:path).join '; '} will go to #{@io.io.path} to be fed to this process"
|
@@ -110,8 +105,8 @@ module Ffmprb
|
|
110
105
|
Ffmprb.logger.debug "Looping #{buff_ios.size} times"
|
111
106
|
|
112
107
|
Ffmprb.logger.debug "(L4) Looping (#{buff_ios.map &:path}) into (#{aux_io.path})"
|
113
|
-
begin
|
114
|
-
Ffmprb.process parent: @raw.process
|
108
|
+
begin # NOTE may not write its entire output, it's ok
|
109
|
+
Ffmprb.process parent: @raw.process, ignore_broken_pipes: false do
|
115
110
|
|
116
111
|
ins = buff_ios.map{ |i| input i }
|
117
112
|
output(aux_io, video: nil, audio: nil) do
|
@@ -119,8 +114,8 @@ module Ffmprb
|
|
119
114
|
end
|
120
115
|
|
121
116
|
end
|
122
|
-
|
123
|
-
|
117
|
+
rescue Util::BrokenPipeError
|
118
|
+
looping_max = false # NOTE see the above warning
|
124
119
|
end
|
125
120
|
end
|
126
121
|
|
@@ -16,7 +16,7 @@ module Ffmprb
|
|
16
16
|
args.concat %W[-pix_fmt #{pixel_format}] if (pixel_format = video.delete(:pixel_format))
|
17
17
|
video.delete :resolution # NOTE is handled otherwise
|
18
18
|
video.delete :fps # NOTE is handled otherwise
|
19
|
-
|
19
|
+
Util.assert_options_empty! video
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
@@ -25,7 +25,8 @@ module Ffmprb
|
|
25
25
|
[].tap do |args|
|
26
26
|
encoder = nil
|
27
27
|
args.concat %W[-c:a #{encoder}] if (encoder = audio.delete(:encoder))
|
28
|
-
|
28
|
+
args.concat %W[-ar #{sampling_freq}] if (sampling_freq = audio.delete(:sampling_freq))
|
29
|
+
Util.assert_options_empty! audio
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
@@ -83,6 +84,7 @@ module Ffmprb
|
|
83
84
|
# NOTE Image-Padding to match the target resolution
|
84
85
|
# TODO full screen only at the moment (see exception above)
|
85
86
|
|
87
|
+
Ffmprb.logger.debug "#{self} asking for filters of #{curr_reel.reel.io.inspect} video: #{channel(:video)}, audio: #{channel(:audio)}"
|
86
88
|
@filters.concat(
|
87
89
|
curr_reel.reel.filters_for lbl, video: channel(:video), audio: channel(:audio)
|
88
90
|
)
|
@@ -315,6 +317,10 @@ module Ffmprb
|
|
315
317
|
end
|
316
318
|
end
|
317
319
|
|
320
|
+
def input(io, video: true, audio: true)
|
321
|
+
process.input io, video: video, audio: audio
|
322
|
+
end
|
323
|
+
|
318
324
|
def roll(
|
319
325
|
reel,
|
320
326
|
onto: :full_screen,
|
data/lib/ffmprb/util.rb
CHANGED
@@ -6,6 +6,7 @@ module Ffmprb
|
|
6
6
|
|
7
7
|
module Util
|
8
8
|
|
9
|
+
class BrokenPipeError < Error; end
|
9
10
|
class TimeLimitError < Error; end
|
10
11
|
|
11
12
|
class << self
|
@@ -41,8 +42,12 @@ module Ffmprb
|
|
41
42
|
value = wait_thr.value
|
42
43
|
status = value.exitstatus # NOTE blocking
|
43
44
|
if status != 0
|
44
|
-
if
|
45
|
-
|
45
|
+
if value.signaled? && value.termsig == Signal.list['PIPE']
|
46
|
+
if ignore_broken_pipes
|
47
|
+
Ffmprb.logger.info "Ignoring broken pipe: #{cmd_str}"
|
48
|
+
else
|
49
|
+
fail BrokenPipeError, cmd_str
|
50
|
+
end
|
46
51
|
else
|
47
52
|
status ||= "sig##{value.termsig}"
|
48
53
|
fail Error, "#{cmd_str} (#{status}):\n#{stderr_r.read}"
|
@@ -63,6 +68,9 @@ module Ffmprb
|
|
63
68
|
thr.value
|
64
69
|
end
|
65
70
|
|
71
|
+
def assert_options_empty!(opts)
|
72
|
+
fail ArgumentError, "Unknown options: #{opts}" unless opts.empty?
|
73
|
+
end
|
66
74
|
protected
|
67
75
|
|
68
76
|
# NOTE a best guess kinda method
|
data/lib/ffmprb/util/proc_vis.rb
CHANGED
@@ -115,6 +115,7 @@ module Ffmprb
|
|
115
115
|
prev_t = Time.now
|
116
116
|
while @_proc_vis_upq.deq # NOTE currently, runs forever (nil terminator needed)
|
117
117
|
proc_vis_do_update
|
118
|
+
Thread.current.live! # XXX not the best we can do here
|
118
119
|
while Time.now - prev_t < UPDATE_PERIOD_SEC
|
119
120
|
@_proc_vis_upq.deq # NOTE drains the queue
|
120
121
|
end
|
@@ -20,6 +20,9 @@ module Ffmprb
|
|
20
20
|
end
|
21
21
|
|
22
22
|
|
23
|
+
attr_reader :stats
|
24
|
+
|
25
|
+
|
23
26
|
# NOTE input/output can be lambdas for single asynchronic io evaluation
|
24
27
|
# the lambdas must be timeout-interrupt-safe (since they are wrapped in timeout blocks)
|
25
28
|
# NOTE all ios are being opened and closed as soon as possible
|
@@ -33,7 +36,6 @@ module Ffmprb
|
|
33
36
|
OpenStruct.new _io: outp, q: SizedQueue.new(ThreadedIoBuffer.blocks_max)
|
34
37
|
end
|
35
38
|
@stats = Stats.new(self)
|
36
|
-
@terminate = false
|
37
39
|
@keep_outputs_open_on_input_idle_limit = keep_outputs_open_on_input_idle_limit
|
38
40
|
# @events = {}
|
39
41
|
|
@@ -45,7 +47,7 @@ module Ffmprb
|
|
45
47
|
end
|
46
48
|
|
47
49
|
Thread.join_children!.tap do
|
48
|
-
Ffmprb.logger.debug "ThreadedIoBuffer (#{@input.path}->#{@outputs.map(&:io).map(&:path)}) terminated successfully (#{
|
50
|
+
Ffmprb.logger.debug "ThreadedIoBuffer (#{@input.path}->#{@outputs.map(&:io).map(&:path)}) terminated successfully (#{stats})"
|
49
51
|
end
|
50
52
|
end
|
51
53
|
end
|
@@ -66,7 +68,7 @@ module Ffmprb
|
|
66
68
|
# handle_synchronously :once
|
67
69
|
#
|
68
70
|
# def reader_done!
|
69
|
-
# Ffmprb.logger.debug "ThreadedIoBuffer reader terminated (#{
|
71
|
+
# Ffmprb.logger.debug "ThreadedIoBuffer reader terminated (#{stats})"
|
70
72
|
# fire! :reader_done
|
71
73
|
# end
|
72
74
|
#
|
@@ -92,7 +94,7 @@ module Ffmprb
|
|
92
94
|
#
|
93
95
|
|
94
96
|
def label
|
95
|
-
"IObuff: Curr/Peak/Max=#{
|
97
|
+
"IObuff: Curr/Peak/Max=#{stats.blocks_buff}/#{stats.blocks_max}/#{ThreadedIoBuffer.blocks_max} In/Out=#{stats.bytes_in}/#{stats.bytes_out}"
|
96
98
|
end
|
97
99
|
|
98
100
|
private
|
@@ -123,61 +125,64 @@ module Ffmprb
|
|
123
125
|
Thread.new("buffer reader") do
|
124
126
|
begin
|
125
127
|
input_io = reader_input!
|
126
|
-
loop do
|
128
|
+
loop do # NOTE until EOFError, see below
|
127
129
|
s = ''
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
if
|
138
|
-
|
139
|
-
|
140
|
-
s = '' # NOTE let's see if it helps outputting an incomplete block
|
141
|
-
else
|
142
|
-
Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) giving up after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{@stats.bytes_in}b closing outputs"
|
143
|
-
@terminate = true
|
144
|
-
output_enq! nil # NOTE EOF signal
|
145
|
-
end
|
130
|
+
while s.length < ThreadedIoBuffer.block_size
|
131
|
+
timeouts = 0
|
132
|
+
logged_timeouts = 1
|
133
|
+
begin
|
134
|
+
ss = input_io.read_nonblock(ThreadedIoBuffer.block_size - s.length)
|
135
|
+
stats.add_bytes_in ss.length
|
136
|
+
s += ss
|
137
|
+
rescue IO::WaitReadable
|
138
|
+
if @keep_outputs_open_on_input_idle_limit && stats.bytes_in > 0 && stats.blocks_buff == 0 && timeouts * ThreadedIoBuffer.io_wait_timeout > @keep_outputs_open_on_input_idle_limit
|
139
|
+
if s.length > 0 # NOTE let's see if it helps outputting an incomplete block
|
140
|
+
Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) giving a chance to write #{s.length}/#{ThreadedIoBuffer.block_size}b after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{stats.bytes_in}b"
|
141
|
+
break
|
146
142
|
else
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
143
|
+
Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) giving up after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{stats.bytes_in}b, closing outputs"
|
144
|
+
raise EOFError
|
145
|
+
end
|
146
|
+
else
|
147
|
+
Thread.current.live!
|
148
|
+
timeouts += 1
|
149
|
+
if timeouts > 2 * logged_timeouts
|
150
|
+
Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) retrying... (#{timeouts} reads): #{$!.class}"
|
151
|
+
logged_timeouts = timeouts
|
154
152
|
end
|
155
|
-
|
156
|
-
Ffmprb.logger.error "ThreadedIoBuffer reader (from #{input_io.path}) gets a #{$!} - should not really happen."
|
157
|
-
IO.select nil, [input_io], nil, ThreadedIoBuffer.io_wait_timeout
|
153
|
+
IO.select [input_io], nil, nil, ThreadedIoBuffer.io_wait_timeout
|
158
154
|
retry
|
159
155
|
end
|
156
|
+
rescue EOFError
|
157
|
+
output_enq! s
|
158
|
+
raise
|
159
|
+
rescue IO::WaitWritable # NOTE should not really happen, so just for conformance
|
160
|
+
Ffmprb.logger.error "ThreadedIoBuffer reader (from #{input_io.path}) gets a #{$!} - should not really happen."
|
161
|
+
IO.select nil, [input_io], nil, ThreadedIoBuffer.io_wait_timeout
|
162
|
+
retry
|
160
163
|
end
|
161
|
-
ensure
|
162
|
-
output_enq! s unless @terminate
|
163
164
|
end
|
165
|
+
output_enq! s
|
164
166
|
end
|
165
167
|
rescue EOFError
|
166
|
-
|
167
|
-
Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) breaking off"
|
168
|
-
@terminate = true
|
169
|
-
output_enq! nil # NOTE EOF signal
|
170
|
-
end
|
168
|
+
Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) breaking off"
|
171
169
|
rescue AllOutputsBrokenError
|
172
170
|
Ffmprb.logger.info "All outputs broken"
|
171
|
+
rescue Exception
|
172
|
+
@reader_failed = Error.new("Reader failed: #{$!}")
|
173
|
+
raise
|
173
174
|
ensure
|
174
175
|
begin
|
175
|
-
|
176
|
+
output_enq! nil # NOTE EOF signal
|
177
|
+
rescue
|
178
|
+
end
|
179
|
+
begin
|
180
|
+
input_io.close if input_io.respond_to?(:close)
|
176
181
|
rescue
|
177
182
|
Ffmprb.logger.error "#{$!.class.name} closing ThreadedIoBuffer input: #{$!.message}"
|
178
183
|
end
|
179
184
|
# reader_done!
|
180
|
-
Ffmprb.logger.debug "ThreadedIoBuffer reader terminated (#{
|
185
|
+
Ffmprb.logger.debug "ThreadedIoBuffer reader terminated (#{stats})"
|
181
186
|
end
|
182
187
|
end
|
183
188
|
end
|
@@ -189,7 +194,7 @@ module Ffmprb
|
|
189
194
|
Ffmprb.logger.debug "Opening buffer output"
|
190
195
|
output.io =
|
191
196
|
Thread.timeout_or_live nil, log: "in the buffer writer helper thread", timeout: ThreadedIoBuffer.timeout do |time|
|
192
|
-
fail Error, "giving up buffer writer init since the reader has failed (#{@
|
197
|
+
fail Error, "giving up buffer writer init since the reader has failed (#{@reader_failed.message})" if @reader_failed
|
193
198
|
output._io.call
|
194
199
|
end
|
195
200
|
Ffmprb.logger.debug "Opened buffer output: #{output.io.path}"
|
@@ -201,21 +206,21 @@ module Ffmprb
|
|
201
206
|
Thread.new("buffer writer") do
|
202
207
|
begin
|
203
208
|
output_io = writer_output!(output)
|
204
|
-
while s = output
|
205
|
-
@stats.blocks_for output, output.q.length
|
209
|
+
while s = output_deq!(output) # NOTE until EOF signal
|
206
210
|
timeouts = 0
|
207
211
|
logged_timeouts = 1
|
208
212
|
begin
|
209
|
-
fail @
|
210
|
-
written = output_io.write_nonblock(s)
|
211
|
-
|
213
|
+
fail @reader_failed if @reader_failed # NOTE otherwise, output_io should not be nil
|
214
|
+
written = output_io.write_nonblock(s)
|
215
|
+
stats.add_bytes_out written
|
212
216
|
|
213
|
-
if written != s.length
|
217
|
+
if written != s.length
|
214
218
|
s = s[written..-1]
|
215
219
|
raise IO::EAGAINWaitWritable
|
216
220
|
end
|
217
221
|
|
218
222
|
rescue IO::WaitWritable
|
223
|
+
Thread.current.live!
|
219
224
|
timeouts += 1
|
220
225
|
if timeouts > 2 * logged_timeouts
|
221
226
|
Ffmprb.logger.debug "ThreadedIoBuffer writer (to #{output_io.path}) retrying... (#{timeouts} writes): #{$!.class}"
|
@@ -236,12 +241,12 @@ module Ffmprb
|
|
236
241
|
ensure
|
237
242
|
# terminated!
|
238
243
|
begin
|
239
|
-
|
240
|
-
output.broken = true
|
244
|
+
output_io.close if !output.broken && output_io && output_io.respond_to?(:close)
|
241
245
|
rescue
|
242
246
|
Ffmprb.logger.error "#{$!.class.name} closing ThreadedIoBuffer output: #{$!.message}"
|
243
247
|
end
|
244
|
-
|
248
|
+
output.broken = true
|
249
|
+
Ffmprb.logger.debug "ThreadedIoBuffer writer (to #{output_io && output_io.path}) terminated (#{stats})"
|
245
250
|
end
|
246
251
|
end
|
247
252
|
end
|
@@ -263,8 +268,7 @@ module Ffmprb
|
|
263
268
|
Timeout.timeout(ThreadedIoBuffer.timeout) do
|
264
269
|
output.q.enq item
|
265
270
|
end
|
266
|
-
|
267
|
-
true
|
271
|
+
stats.blocks_for output
|
268
272
|
|
269
273
|
rescue Timeout::Error
|
270
274
|
next if output.broken
|
@@ -277,19 +281,26 @@ module Ffmprb
|
|
277
281
|
|
278
282
|
retry unless timeouts >= ThreadedIoBuffer.timeout_limit # NOTE the queue has probably overflown
|
279
283
|
|
280
|
-
@
|
284
|
+
@reader_failed ||= Error.new("the writer has failed with timeout limit while queuing") # NOTE screw the race condition
|
281
285
|
# timeout!
|
282
286
|
fail Error, "Looks like we're stuck (>#{ThreadedIoBuffer.timeout_limit*ThreadedIoBuffer.timeout}s idle) with #{ThreadedIoBuffer.blocks_max}x#{ThreadedIoBuffer.block_size}b blocks (buffering #{reader_input!.path}->...)..."
|
283
287
|
end
|
284
288
|
end.empty?
|
285
289
|
end
|
286
290
|
|
291
|
+
def output_deq!(outp)
|
292
|
+
outp.q.deq.tap do
|
293
|
+
stats.blocks_for outp
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
287
297
|
class Stats < OpenStruct
|
288
298
|
include MonitorMixin
|
289
299
|
|
290
300
|
def initialize(proc)
|
291
301
|
@proc = proc
|
292
|
-
|
302
|
+
@output_blocks = {}
|
303
|
+
super blocks_buff: 0, blocks_max: 0, bytes_in: 0, bytes_out: 0
|
293
304
|
end
|
294
305
|
|
295
306
|
def add_bytes_in(n)
|
@@ -306,14 +317,14 @@ module Ffmprb
|
|
306
317
|
end
|
307
318
|
end
|
308
319
|
|
309
|
-
def blocks_for(outp
|
320
|
+
def blocks_for(outp)
|
310
321
|
synchronize do
|
311
|
-
|
312
|
-
|
322
|
+
blocks = @output_blocks[outp.object_id] = outp.q.length
|
323
|
+
if blocks > blocks_max
|
324
|
+
self.blocks_max = blocks
|
313
325
|
@proc.proc_vis_node @proc # NOTE update
|
314
326
|
end
|
315
|
-
(
|
316
|
-
self.blocks_buff = @_outp_blocks.values.reduce(0, :+)
|
327
|
+
self.blocks_buff = @output_blocks.values.reduce(0, :+)
|
317
328
|
end
|
318
329
|
end
|
319
330
|
|
data/lib/ffmprb/version.rb
CHANGED
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.
|
4
|
+
version: 0.11.2
|
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: 2016-04-
|
12
|
+
date: 2016-04-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mkfifo
|
@@ -25,6 +25,20 @@ dependencies:
|
|
25
25
|
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: 0.1.1
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: thor
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.19.1
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.19.1
|
28
42
|
- !ruby/object:Gem::Dependency
|
29
43
|
name: bundler
|
30
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -45,28 +59,28 @@ dependencies:
|
|
45
59
|
requirements:
|
46
60
|
- - ">="
|
47
61
|
- !ruby/object:Gem::Version
|
48
|
-
version: 8.2.
|
62
|
+
version: 8.2.4
|
49
63
|
type: :development
|
50
64
|
prerelease: false
|
51
65
|
version_requirements: !ruby/object:Gem::Requirement
|
52
66
|
requirements:
|
53
67
|
- - ">="
|
54
68
|
- !ruby/object:Gem::Version
|
55
|
-
version: 8.2.
|
69
|
+
version: 8.2.4
|
56
70
|
- !ruby/object:Gem::Dependency
|
57
71
|
name: simplecov
|
58
72
|
requirement: !ruby/object:Gem::Requirement
|
59
73
|
requirements:
|
60
74
|
- - ">="
|
61
75
|
- !ruby/object:Gem::Version
|
62
|
-
version: 0.
|
76
|
+
version: 0.11.2
|
63
77
|
type: :development
|
64
78
|
prerelease: false
|
65
79
|
version_requirements: !ruby/object:Gem::Requirement
|
66
80
|
requirements:
|
67
81
|
- - ">="
|
68
82
|
- !ruby/object:Gem::Version
|
69
|
-
version: 0.
|
83
|
+
version: 0.11.2
|
70
84
|
- !ruby/object:Gem::Dependency
|
71
85
|
name: guard-rspec
|
72
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -151,7 +165,8 @@ dependencies:
|
|
151
165
|
- - ">="
|
152
166
|
- !ruby/object:Gem::Version
|
153
167
|
version: 0.2.6
|
154
|
-
description: A DSL (Damn-Simple Language) and a micro-engine
|
168
|
+
description: A video and audio composing DSL (Damn-Simple Language) and a micro-engine
|
169
|
+
for ffmpeg and ffriends
|
155
170
|
email:
|
156
171
|
- costa@mouldwarp.com
|
157
172
|
executables:
|