ffmprb 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  GIT
2
2
  remote: http://git/ffmprb/.git
3
- revision: cafbbe2c33cb9eca853412f1f96aafbe428abcdc
3
+ revision: d114fb42b56d213aa671578deec948ac687f0a0f
4
4
  specs:
5
- ffmprb (0.11.5)
5
+ ffmprb (0.12.1)
6
6
  mkfifo (~> 0.1.1)
7
7
  thor (~> 0.19.1)
8
8
 
@@ -8,7 +8,7 @@ VIDEO_OPT = {resolution: Ffmprb::HD_1080p, fps: 30}
8
8
 
9
9
 
10
10
  # XXX
11
- def dura_to_sec(dura_str)
11
+ def dura_to(dura_str)
12
12
  dura_str.split(':').reverse.each_with_index.reduce(0) do |sec, (ns, i)|
13
13
  sec + ns.to_i*(60**i)
14
14
  end
@@ -27,8 +27,14 @@ begin
27
27
  warn "\nCut-catting to #{out_path}..."
28
28
 
29
29
  Ffmprb.process do
30
+ inp = input(inp_path)
30
31
  output out_path, video: VIDEO_OPT do
31
- roll input(inp_path).cut to: 60 # XXX .crop(top: 0.35, bottom: 0.15, left: 0.15, right: 0.35).cut from: 10, to: 20
32
+ roll inp.cut to: 16
33
+ roll inp.cut(from: 16, to: 32).pace(2)
34
+ roll inp.cut(from: 32, to: 64).pace(4)
35
+ roll inp.cut(from: 64, to: 80).pace(2)
36
+ roll inp.cut from: 80, to: 160
37
+ # roll input(inp_path).cut to: 60 # XXX .crop(top: 0.35, bottom: 0.15, left: 0.15, right: 0.35).cut from: 10, to: 20
32
38
  end
33
39
  end
34
40
  end
data/exp/youtubby/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
- gem 'ffmprb', :git => 'http://git/ffmprb/.git', :branch => 'pacing'
1
+ gem 'ffmprb', :git => 'http://git/ffmprb/.git', :branch => 'pace-fix'
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem 'google-apis-youtube_v3'
6
-
7
- gem 'byebug' # XXX
@@ -1,9 +1,9 @@
1
1
  GIT
2
2
  remote: http://git/ffmprb/.git
3
- revision: 90ab02d748e2f86211b6cfd60cf35741deac318c
4
- branch: pacing
3
+ revision: aebc01b05ca81ce3a0c249631792d65feb854854
4
+ branch: pace-fix
5
5
  specs:
6
- ffmprb (0.12.1)
6
+ ffmprb (0.12.2)
7
7
  mkfifo (~> 0.1.1)
8
8
  thor (~> 0.19.1)
9
9
 
@@ -12,7 +12,6 @@ GEM
12
12
  specs:
13
13
  addressable (2.8.1)
14
14
  public_suffix (>= 2.0.2, < 6.0)
15
- byebug (11.1.3)
16
15
  declarative (0.0.20)
17
16
  faraday (2.7.2)
18
17
  faraday-net_http (>= 2.0, < 3.1)
@@ -65,7 +64,6 @@ PLATFORMS
65
64
  ruby
66
65
 
67
66
  DEPENDENCIES
68
- byebug
69
67
  ffmprb!
70
68
  google-apis-youtube_v3
71
69
 
@@ -0,0 +1,356 @@
1
+
2
+ # The general idea of (a video production pipeline for) "vlogging":
3
+ # -1. think of something nice to shoot
4
+ # - maybe even write down a scenario and produce "the picture"
5
+ # 0. shoot the damn footage with your cam/mic/what-have-you
6
+ # - press Record on your GoPro (video stored on device)
7
+ # 1. upload the raw media files to a (cloud) storage
8
+ # - GoPro media storage subscription (video files accessible at)
9
+ # 2. take note of, sort and clean the raw footage up manually
10
+ # - cut and concatenate the media files into cohesive "scenes" (ffmprb script)
11
+ # 3. upload the HD renders of the raw scenes
12
+ # - to a "cache" storage (ghlr media), plus, for the public access (YouTube)
13
+ # 4. compose a video from the raw scene cuts and additional media
14
+ # - create a (ffmprb) montage script using the media
15
+ # 5. upload and premiere the video piece versions (final cut, general public)
16
+ # - to "ghlr media sharing" (fut.), plus, for public "discovery" (YouTube)
17
+ # 6. profit?
18
+
19
+ # # empty (micro) case is raw publish
20
+
21
+ # NOTE on simprender:
22
+ # a title-back-summary effect
23
+ # XXX
24
+ # -16(2) -8(2) -4(2) ^0 2(2), -4(1) [1]
25
+ # crop
26
+ # ?soundtrack?
27
+ # cut(from 24, to: 56).pace(16).reverse
28
+ # cut(from 8, to: 24).pace(8).reverse
29
+ # cut(from 0, to: 8).pace(4).reverse
30
+ # cut(from: 0, to: 4).pace(2)
31
+ # cut(from: 0, to: 4).pace(4).reverse
32
+ #
33
+ # 2.0
34
+ # crop(0.48).cut(from: M).pace(m).reverse
35
+ # ...
36
+ # crop(0.2).cut(from: 12, to: 28).pace(16).reverse
37
+ # crop(0.18).cut(from: 4, to: 12).pace(8).reverse
38
+ # crop(0.15).cut(from: 0, to: 4).pace(4).reverse
39
+ # crop(0.1).cut(from: 0, to: 4).pace(2)
40
+ # crop(0.05).cut(from: 0, to: 4).pace(4).reverse
41
+ #
42
+ # 3.0 (aka 1.11)
43
+ # crop(0.49).cut(from: M).pace(m).reverse
44
+ # ...
45
+ # crop(0.2).cut(from: 12, to: 28).pace(16).reverse
46
+ # crop(0.25).cut(from: 4, to: 12).pace(8).reverse
47
+ # crop(0.14).cut(from: 0, to: 4).pace(4).reverse
48
+
49
+ # NOTE looking good?
50
+ REV_CUT_CROP_EXP_BASE = 1.333
51
+
52
+ # NOTE unreasonable to exhaust
53
+ REV_CUT_CROP_EXP_PRE_CUTS =
54
+ (2..64).reduce [] do |a, k|
55
+ if (3.6..3600.0).include? (int = REV_CUT_CROP_EXP_BASE**k)
56
+ a << (a[-1] || 0) + int
57
+ else
58
+ a
59
+ end
60
+ end.reverse
61
+
62
+ # NOTE reasonable to look good
63
+ REV_CUT_CROP_LENGTH_MIN_S = REV_CUT_CROP_EXP_PRE_CUTS[-4]
64
+
65
+ # XXX Can also go hard on with blending to complement the rewind effect...
66
+
67
+ def rev_cut_crop_seq(length, inp)
68
+ fail "supply a block that receives the next reel and returns the next input" unless
69
+ block_given?
70
+
71
+ return unless
72
+ length > REV_CUT_CROP_LENGTH_MIN_S
73
+
74
+ REV_CUT_CROP_EXP_PRE_CUTS.each_with_index do |k, i|
75
+ next if
76
+ (n = REV_CUT_CROP_EXP_PRE_CUTS[i + 1] || 0) > length
77
+ cut = {from: n}
78
+ cut[:to] = k unless
79
+ k > length
80
+ pace = REV_CUT_CROP_EXP_BASE**(REV_CUT_CROP_EXP_PRE_CUTS.length - i + 1)
81
+ # XXX Math.log(1 + (REV_CUT_CROP_EXP_PRE_CUTS.length - i + 2) / 5.0) / 4 # NOTE magick!
82
+ crop = 0.5*(1 - Math.sqrt(1/pace)) # NOTE bitrate! (x2)
83
+ # inp = yield(inp.crop(crop).cut(cut).pace(pace).reverse)
84
+ yield inp.crop(crop).cut(cut).pace(pace).reverse
85
+ end
86
+ # XXX
87
+ # inp = yield(inp.crop(0.1).cut(from: 0, to: 4).pace(2))
88
+ # inp = yield(inp.crop(0.05).cut(from: 0, to: 4).pace(4).reverse)
89
+ end
90
+
91
+ channel = ARGV.shift || 'default'
92
+
93
+ abort "USAGE: gop-raw-cut-you-HD60.rb [CHANNEL]" unless
94
+ ARGV.empty?
95
+
96
+ MEDIA_DIR = ENV['MEDIA_DIR'] or
97
+ abort "MEDIA_DIR needed"
98
+
99
+ now_time = Time.now
100
+ title = "XXX Topublish (uploaded at #{now_time.strftime '%y-%m-%d-%H-%M'})"
101
+
102
+ require 'cgi'
103
+
104
+ WE_URI = 'http://localhost'
105
+
106
+ require_relative '../google_youtube'
107
+
108
+ youtube = google_youtube(WE_URI, channel) do |auth_url|
109
+ puts "Open the following URL in your browser and authorize the application."
110
+ puts "(you'll have to copy the code= URL param when redrircted to #{WE_URI})"
111
+ puts auth_url
112
+ puts "Paste the authorization code."
113
+ $stdin.gets.chomp
114
+ end
115
+
116
+ require 'fileutils'
117
+
118
+ require 'ffmprb'
119
+ Ffmprb.debug = true # XXX
120
+ Ffmprb::Util::Thread.timeout = 150
121
+
122
+ int_video_opt = {resolution: Ffmprb::HD_4K, fps: 60}
123
+ raw_video_opt = {resolution: Ffmprb::HD_4K, fps: 60, encoder: 'libx264 -crf 15'} # XXX -preset superfast
124
+ fin_video_opt = {resolution: Ffmprb::HD_1080p, fps: 60, encoder: 'libx265 -crf 19'} # XXX 21 -preset veryslow
125
+ YOU_VIDEO_OPT = {resolution: Ffmprb::HD_4K, fps: 60, encoder: 'libx264 -crf 17'} # XXX 15 -preset superfast
126
+
127
+ GOP_FILE_PREFIX = 'GX'
128
+ GOP_MP4_RE = /\b(#{GOP_FILE_PREFIX}(\d\d)(\d\d\d\d)\.MP4)\b/i
129
+ GOP_ZIP_URL_RE = %r[/zip/]i
130
+
131
+
132
+ def dura_to_sec(dura_str)
133
+ dura_str.split(':').reverse.each_with_index.reduce(0) do |sec, (ns, i)|
134
+ sec + ns.to_i*(60**i)
135
+ end
136
+ end
137
+
138
+ def date_media_path(date, name)
139
+ File.join MEDIA_DIR, '%04d' % date.year, '%02d' % date.month, '%02d' % date.day, "#{name}.mp4"
140
+ end
141
+
142
+ raw_time = nil
143
+
144
+ warn "FYI"
145
+ system "df -h"
146
+ warn "\nGo to https://plus.gopro.com/media-library" +
147
+ "\nEnter GoP video D/L URLs followed (same line) by cut times, space-separated:\n\n"
148
+
149
+ av_src_cuts = []
150
+ int_av_path = nil
151
+ dl_q = Queue.new
152
+
153
+ fetcher = Thread.new do
154
+ while (url, cuts = dl_q.deq)
155
+ srcs = []
156
+ shot = nil
157
+ while srcs.empty? # NOTE sometimes (zip) D/L silently fails, see below
158
+ name =
159
+ case CGI.unescape url
160
+ when GOP_ZIP_URL_RE
161
+ 'tmp.zip'
162
+ when GOP_MP4_RE
163
+ $1
164
+ else
165
+ abort "ERROR invalid URL, cannot go on"
166
+ end
167
+ abort "ERROR downloading #{url}" unless
168
+ system "curl -so #{name} '#{url}'"
169
+ if name == 'tmp.zip'
170
+ zip_lines = `unzip -o #{name}`.lines
171
+ if $?.success? # NOTE if the D/L in fact has failed, it'll be retried
172
+ zip_lines.each do |line|
173
+ if line =~ GOP_MP4_RE
174
+ srcs << $1
175
+ shot = $3
176
+ end
177
+ end
178
+ end
179
+ File.delete name
180
+ else
181
+ srcs << name
182
+ shot = $3
183
+ end
184
+ end
185
+
186
+ # NOTE assuming srcs is not empty, for the first src
187
+ unless raw_time
188
+ raw_time =
189
+ Ffmprb::File.access(srcs[0]).creation_time ||
190
+ now_time # NOTE well...
191
+ int_av_path = date_media_path(raw_time, "#{GOP_FILE_PREFIX}-#{shot}")
192
+ # XXX
193
+ # break if
194
+ # (Ffmprb::File.access(int_av_path).length rescue 0) > 0
195
+ end
196
+
197
+ av_src_cuts << [
198
+ srcs.sort do |a, b|
199
+ a_m = GOP_MP4_RE.match(a)
200
+ b_m = GOP_MP4_RE.match(b)
201
+ if (fst = a_m[3] <=> b_m[3]) != 0
202
+ fst
203
+ else
204
+ a_m[2] <=> b_m[2]
205
+ end
206
+ end,
207
+ cuts
208
+ ]
209
+ end
210
+
211
+ abort "ERROR no inputs given" unless
212
+ int_av_path
213
+ end
214
+
215
+ while (url_cut = gets)
216
+ url, *cut = url_cut.chomp.split(' ')
217
+ next if url.split('#')[0].empty?
218
+
219
+ last_cut = -1
220
+ cuts = cut.map do |ns|
221
+ dura_to_sec(ns).tap do |curr_cut|
222
+ abort "ERROR cut times must be ascending (look it up)" unless
223
+ curr_cut > last_cut
224
+ end
225
+ end
226
+ dl_q.enq [url, cuts]
227
+ end
228
+ dl_q.enq nil
229
+
230
+ warn "\nFetching those files..."
231
+ fetcher.join
232
+
233
+ out_path = date_media_path(now_time, "#{GOP_FILE_PREFIX}-#{raw_time.strftime '%y-%m-%d-%H-%M'}-simprender")
234
+
235
+ Ffmprb::File.temp '.mp4' do |you_out_path|
236
+ warn "\nCut-catting to cache (#{int_av_path})"
237
+
238
+ pipe_cut_threads = av_src_cuts.map do |srcs, cuts|
239
+ [
240
+ (tmp_av_stream = Ffmprb::File.temp_fifo('.flv')),
241
+ cuts.each_slice(2).map { |from, to| {from: from, to: to} },
242
+ Thread.new do
243
+ Ffmprb.process do
244
+ output tmp_av_stream, video: int_video_opt do
245
+ srcs.each do |src|
246
+ roll input src
247
+ end
248
+ end
249
+ end
250
+ end
251
+ ]
252
+ end
253
+
254
+ Ffmprb::File.temp_fifo('.flv') do |av_stream|
255
+ thr = Thread.new do
256
+ unless pipe_cut_threads.empty?
257
+ FileUtils.mkdir_p File.dirname int_av_path
258
+ Ffmprb.process do
259
+ inp_cut_opts =
260
+ pipe_cut_threads.map do |tmp_av_stream, cut_opts, _|
261
+ (cut_opts.empty?? [{}] : cut_opts).map do |cut_opt|
262
+ [input(tmp_av_stream), cut_opt]
263
+ end
264
+ end.reduce :+
265
+ # XXX output int_av_path, video: raw_video_opt do
266
+ output av_stream, video: raw_video_opt do
267
+ inp_cut_opts.each do |inp, cut_opt|
268
+ roll inp.cut cut_opt
269
+ end
270
+ end
271
+ end
272
+
273
+ # XXX this is flawed:
274
+ # the simple threads fail because of oom docker signals (9)
275
+ # (not because of broken pipes)
276
+ pipe_cut_threads.each do |tmp_av_stream, _, thr|
277
+ begin
278
+ thr.join
279
+ rescue
280
+ warn "WARN errors-a-happening: #{$!}"
281
+ end
282
+ tmp_av_stream.unlink
283
+ end
284
+ end
285
+
286
+ # XXX
287
+ # warn "\nComposing and rendering to out (#{out_path}) + you..."
288
+ # Ffmprb.process do
289
+ # output av_stream, video: int_video_opt do
290
+ # # XXX roll in1.crop(0.25).cut(from: Ffmprb::File.access(int_av_path).length - 512).pace(16).reverse
291
+
292
+ # rev_cut_crop_seq(Ffmprb::File.access(int_av_path).length, input(int_av_path)) do |reel|
293
+ # roll reel
294
+ # end
295
+ # roll input(int_av_path)
296
+ # end
297
+ # end
298
+ # [Rational(1)/8, Rational(1)/4, Rational(1)/2, 1, 2, 4, 8].each do |r| # XXX
299
+ # output out_path("#{out_name}x#{r.to_f}"), video: fin_video_opt do
300
+ # inp_cut_opts.each do |inp, cut_opt|
301
+ # roll inp.cut(cut_opt).pace r
302
+ # end
303
+ # end
304
+ # end
305
+
306
+ # XXX
307
+ # output out_path("#{out_name}x-8"), video: fin_video_opt do
308
+ # inp_cut_opts.each do |inp, cut_opt|
309
+ # roll inp.cut(cut_opt).reverse.pace 8
310
+ # end
311
+ # end
312
+ # output out_path("#{out_name}x8-"), video: fin_video_opt do
313
+ # inp_cut_opts.each do |inp, cut_opt|
314
+ # roll inp.cut(cut_opt).pace(8).reverse
315
+ # end
316
+ # end
317
+ end
318
+ # XXX bad romance: abort from any thread
319
+ begin
320
+ FileUtils.mkdir_p File.dirname out_path
321
+ Ffmprb.process do
322
+ in1 = input(av_stream)
323
+ output you_out_path, video: YOU_VIDEO_OPT do
324
+ roll in1
325
+ end
326
+ output out_path, video: fin_video_opt do
327
+ roll in1 # XXX .pp
328
+ end
329
+ end
330
+ ensure
331
+ thr.join
332
+ end
333
+ end
334
+
335
+
336
+ metadata = {
337
+ snippet: {
338
+ title: title
339
+ },
340
+ status: {
341
+ privacy_status: 'private'
342
+ }
343
+ }
344
+
345
+ video =
346
+ youtube.insert_video('snippet,status', metadata, upload_source: you_out_path, content_type: 'video/mp4')
347
+
348
+ if video.status.upload_status == 'uploaded'
349
+ warn "OK"
350
+ else
351
+ warn "FAIL"
352
+ end
353
+ end
354
+ ensure
355
+ FileUtils.rm_r tmp_dir
356
+ end
@@ -10,4 +10,6 @@ docker-compose build --force-rm
10
10
  export GOOGLE_CLIENT_ID="$(< exp/.google-client-id)"
11
11
  export GOOGLE_CLIENT_SECRET="$(< exp/.google-client-secret)"
12
12
 
13
- docker-compose run --rm youtubby exp/gop-raw-cut-you-HD60.rb "$1"
13
+ # XXX docker-compose run --rm youtubby exp/gop-raw-cut-you-HD60.rb "$1"
14
+ docker-compose run --rm youtubby exp/gop-raw-cut-rcc-join-you-HD60.rb "$1"
15
+