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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 47073fdaab1b6fda78751fd6842cd65c24370b96
4
- data.tar.gz: 92580ab7a3d5e2f2e4651305a9570de9156a8e94
3
+ metadata.gz: 428b5ce07b48c960ffe05fded9e45d6f458e405b
4
+ data.tar.gz: 686389df8fd80d665105cdd12682f5f547ec48ba
5
5
  SHA512:
6
- metadata.gz: 84df8ed0c2802b285c5f1a6ee0a6cbb7793d3d2cd239eca7e6aa9f6af649cf04540d8ca4a6698549324cc70b6f2ee7dac354aa3d3262b5d8916fd6dc1ebf0220
7
- data.tar.gz: 63cbc00bad936538d74e1190b47766804f6d583ff4f7061cf48a348fc9e5b11509f8cc8659e6e90751776157f34ecc85655be24787fac038fc9bec08a84e2bab
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/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
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
- Allows for scripts like
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
- in_main = input('flick.mp4')
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.wav -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
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
- Finish by playing your special _outro_:
86
+
87
+ ### In the code
88
+
89
+ The block above is to be given to an `Ffmprb.process` call:
90
+
88
91
  ```ruby
89
- lay outro, transition: {blend: 1}
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
- Anything that follows this order will work -- the script may be generated on the fly:
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
- transitions = [:blend, :burn, :zoom]
95
- photos.shuffle.each do |photo|
96
- lay photo.loop.cut(to: rand * 3), transition: {transitions.shuffle.first => 1}
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
- All _inputs_ mentioned above must be supplied to `Ffmprb::process` as following
100
- (the complete script as can be run with `ffmprb` CLI, see below):
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
- # script.ffmprb
135
+ # episode_make.ffmprb
103
136
  |episode, logo, intro, badge, title, promo, credits, outro|
104
137
 
105
- lay episode.cut(to: 60), transition: {blend: 3}
106
- overlay logo.loop.cut(to: 33), after: 3, transition: {blend: 1}
107
- lay intro, transition: {blend: 1}
108
- overlay badge.loop, at: 1, transition: {burn: 1}
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
- ### Attention
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
- Inside a `process` block, there are input definitions and output definitions;
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 |av_input1, av_output1|
167
+ Ffmprb.process do
132
168
 
133
- in_main = input('flick.mp4')
134
- output('film.flv', video: {resolution: Ffmprb::HD_720p, fps: 25}) do
135
- roll in_main.crop(0.05), transition: {blend: 1}
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
@@ -8,4 +8,4 @@ end
8
8
 
9
9
  require 'ffmprb'
10
10
 
11
- Ffmprb.execute
11
+ Ffmprb::Execution.start
@@ -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 = 'https://github.com/showbox-oss/ffmprb'
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.2'
26
- spec.add_development_dependency 'simplecov', '>= 0.10.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'
@@ -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
 
@@ -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 : Logger::INFO # XXX thorify for -v -vv and -q
42
+ logger.level = debug ? Logger::DEBUG : Ffmprb.log_level
43
43
  end
44
44
  end
45
45
 
@@ -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 initialize(*params, script:)
6
- @params = params
7
- @script = eval("lambda{#{script}}")
8
- end
7
+ def self.exit_on_failure?; true; end
9
8
 
10
- def run
11
- Ffmprb.process *@params, ignore_broken_pipes: false, &@script
12
- end
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
- end
13
+ default_task :process
15
14
 
16
- def self.execute
17
- return STDERR.puts "Usage: (not quite usual) $ ffmprb streams... < script.ffmprb" unless
18
- ARGV.length > 1 && ARGV.grep(/^-/).empty?
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
@@ -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
- fail Error, "Unknown options: #{opts}" unless opts.empty? # XXX refactor into a separate error
119
+ Util.assert_options_empty! opts
118
120
  @inputs, @outputs = [], []
119
121
  end
120
122
 
@@ -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
- fail "Unknown input video options: #{video}" unless video.empty?
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
- fail "Unknown input audio options: #{audio}" unless audio.empty?
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)? Filter.anull("#{in_lbl}:a", "#{lbl}:a"): nil)
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 *buff_ios
97
- Util::Thread.join_children!
98
-
99
- 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 https://github.com/showbox-oss/ffmprb/issues please)." if looping && times == Util.ffmpeg_inputs_max
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 do # NOTE may not write its entire output, it's ok
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
- ensure
123
- looping = false # NOTE see the above warning
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
- fail "Unknown output video options: #{video}" unless video.empty?
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
- fail "Unknown output audio options: #{audio}" unless audio.empty?
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,
@@ -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 ignore_broken_pipes && value.signaled? && value.termsig == Signal.list['PIPE']
45
- Ffmprb.logger.info "Ignoring broken pipe: #{cmd_str}"
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
@@ -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 (#{@stats})"
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 (#{@stats})"
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=#{@stats.blocks_buff}/#{@stats.blocks_max}/#{ThreadedIoBuffer.blocks_max} In/Out=#{@stats.bytes_in}/#{@stats.bytes_out}"
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
- begin
129
- while s.length < ThreadedIoBuffer.block_size
130
- timeouts = 0
131
- logged_timeouts = 1
132
- begin
133
- ss = input_io.read_nonblock(ThreadedIoBuffer.block_size - s.length)
134
- @stats.add_bytes_in ss.length
135
- s += ss
136
- rescue IO::WaitReadable
137
- if !@terminate && @stats.bytes_in > 0 && @stats.blocks_buff == 0 && @keep_outputs_open_on_input_idle_limit && timeouts * ThreadedIoBuffer.io_wait_timeout > @keep_outputs_open_on_input_idle_limit
138
- if s.length > 0
139
- output_enq! s
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
- timeouts += 1
148
- if !@terminate && timeouts > 2 * logged_timeouts
149
- Ffmprb.logger.debug "ThreadedIoBuffer reader (from #{input_io.path}) retrying... (#{timeouts} reads): #{$!.class}"
150
- logged_timeouts = timeouts
151
- end
152
- IO.select [input_io], nil, nil, ThreadedIoBuffer.io_wait_timeout
153
- retry
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
- rescue IO::WaitWritable # NOTE should not really happen, so just for conformance
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
- unless @terminate
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
- reader_input!.close if reader_input!.respond_to?(:close)
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 (#{@stats})"
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 (#{@terminate.message})" if @terminate.kind_of? Exception
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.q.deq # NOTE until EOF signal
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 @terminate if @terminate.kind_of? Exception
210
- written = output_io.write_nonblock(s) if output_io # NOTE will only be nil if @terminate is an exception
211
- @stats.add_bytes_out written
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 # NOTE kinda optimisation
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
- writer_output!(output).close if !output.broken && writer_output!(output).respond_to?(:close)
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
- Ffmprb.logger.debug "ThreadedIoBuffer writer (to #{output_io && output_io.path}) terminated (#{@stats})"
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
- @stats.blocks_for output, output.q.length
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
- @terminate = Error.new("the writer has failed with timeout limit while queuing")
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
- super blocks_max: 0, bytes_in: 0, bytes_out: 0
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, n)
320
+ def blocks_for(outp)
310
321
  synchronize do
311
- if n > blocks_max
312
- self.blocks_max = n
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
- (@_outp_blocks ||= {})[outp] = n
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
 
@@ -1,6 +1,8 @@
1
1
  module Ffmprb
2
- VERSION = '0.10.1'
3
2
 
3
+ VERSION = '0.11.2'
4
+
5
+ GEM_GITHUB_URL = 'https://github.com/showbox-oss/ffmprb'
4
6
 
5
7
  FIREBASE_AVAILABLE =
6
8
  begin
@@ -8,4 +10,5 @@ module Ffmprb
8
10
  true
9
11
  rescue Exception
10
12
  end
13
+
11
14
  end
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.10.1
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-21 00:00:00.000000000 Z
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.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.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.10.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.10.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 for ffmpeg and ffriends
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: