ffmprb 0.10.1 → 0.11.2

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.
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: