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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +10 -10
- data/README.md +7 -2
- data/coverage/index.html +2541 -2123
- data/exp/present/Gemfile.lock +2 -2
- data/exp/present/exp/present.rb +8 -2
- data/exp/youtubby/Gemfile +1 -3
- data/exp/youtubby/Gemfile.lock +3 -5
- data/exp/youtubby/exp/gop-raw-cut-rcc-join-you-HD60.rb +356 -0
- data/exp/youtubby/exp/gop-raw-cut-you-HD60 +3 -1
- data/exp/youtubby/exp/gop-raw-cut-you-HD60.rb +191 -63
- data/exp/youtubby/google_youtube.rb +3 -3
- data/ffmprb.gemspec +2 -0
- data/lib/defaults.rb +1 -1
- data/lib/ffmprb/file/sample.rb +15 -7
- data/lib/ffmprb/file.rb +13 -5
- data/lib/ffmprb/filter.rb +19 -7
- data/lib/ffmprb/process/input/chain_base.rb +5 -0
- data/lib/ffmprb/process/input/cut.rb +0 -4
- data/lib/ffmprb/process/input/paced.rb +3 -1
- data/lib/ffmprb/process/input.rb +4 -4
- data/lib/ffmprb/process/output.rb +2 -11
- data/lib/ffmprb/process.rb +22 -13
- data/lib/ffmprb/util/thread.rb +6 -3
- data/lib/ffmprb/util.rb +8 -6
- data/lib/ffmprb/version.rb +1 -1
- metadata +4 -3
data/exp/present/Gemfile.lock
CHANGED
data/exp/present/exp/present.rb
CHANGED
@@ -8,7 +8,7 @@ VIDEO_OPT = {resolution: Ffmprb::HD_1080p, fps: 30}
|
|
8
8
|
|
9
9
|
|
10
10
|
# XXX
|
11
|
-
def
|
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
|
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
data/exp/youtubby/Gemfile.lock
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
GIT
|
2
2
|
remote: http://git/ffmprb/.git
|
3
|
-
revision:
|
4
|
-
branch:
|
3
|
+
revision: aebc01b05ca81ce3a0c249631792d65feb854854
|
4
|
+
branch: pace-fix
|
5
5
|
specs:
|
6
|
-
ffmprb (0.12.
|
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
|
+
|