video_transcoding 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +573 -0
- data/bin/convert-video +290 -0
- data/bin/detect-crop +130 -0
- data/bin/query-handbrake-log +248 -0
- data/bin/transcode-video +1205 -0
- data/lib/video_transcoding.rb +21 -0
- data/lib/video_transcoding/cli.rb +53 -0
- data/lib/video_transcoding/console.rb +46 -0
- data/lib/video_transcoding/copyright.rb +9 -0
- data/lib/video_transcoding/crop.rb +110 -0
- data/lib/video_transcoding/errors.rb +10 -0
- data/lib/video_transcoding/ffmpeg.rb +47 -0
- data/lib/video_transcoding/handbrake.rb +56 -0
- data/lib/video_transcoding/media.rb +323 -0
- data/lib/video_transcoding/mkvmerge.rb +35 -0
- data/lib/video_transcoding/mkvpropedit.rb +35 -0
- data/lib/video_transcoding/mp4track.rb +35 -0
- data/lib/video_transcoding/mplayer.rb +33 -0
- data/lib/video_transcoding/tool.rb +52 -0
- data/lib/video_transcoding/version.rb +9 -0
- data/video_transcoding.gemspec +22 -0
- metadata +73 -0
data/bin/transcode-video
ADDED
@@ -0,0 +1,1205 @@
|
|
1
|
+
#!/usr/bin/env ruby -W
|
2
|
+
#
|
3
|
+
# transcode-video
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2015 Don Melton
|
6
|
+
#
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
9
|
+
|
10
|
+
require 'fileutils'
|
11
|
+
require 'tmpdir'
|
12
|
+
require 'video_transcoding/cli'
|
13
|
+
|
14
|
+
module VideoTranscoding
|
15
|
+
class Command
|
16
|
+
include CLI
|
17
|
+
|
18
|
+
MAX_WIDTH = 4096
|
19
|
+
MAX_HEIGHT = 2304
|
20
|
+
|
21
|
+
def about
|
22
|
+
<<HERE
|
23
|
+
transcode-video #{VERSION}
|
24
|
+
#{COPYRIGHT}
|
25
|
+
HERE
|
26
|
+
end
|
27
|
+
|
28
|
+
def usage
|
29
|
+
<<HERE
|
30
|
+
Transcode video file or disc image directory into format and size similar to
|
31
|
+
popular online downloads. Works best with Blu-ray or DVD rip.
|
32
|
+
|
33
|
+
Automatically determines target video bitrate, number of audio tracks, etc.
|
34
|
+
WITHOUT ANY command line options.
|
35
|
+
|
36
|
+
Usage: #{$PROGRAM_NAME} [OPTION]... [FILE|DIRECTORY]...
|
37
|
+
|
38
|
+
Input options:
|
39
|
+
--scan list title(s) and tracks in video media and exit
|
40
|
+
--title INDEX select indexed title in video media
|
41
|
+
(default: main feature or first listed)
|
42
|
+
--chapters CHAPTER[-CHAPTER]
|
43
|
+
select chapters, single or range (default: all)
|
44
|
+
|
45
|
+
Output options:
|
46
|
+
-o, --output FILENAME|DIRECTORY
|
47
|
+
set output path and filename, or just path
|
48
|
+
(default: input filename with output format extension
|
49
|
+
in current working directory)
|
50
|
+
--mp4 output MP4 instead of Matroska `.mkv` format
|
51
|
+
--m4v " " with `.m4v` extension instead of `.mp4`
|
52
|
+
--chapter-names FILENAME
|
53
|
+
import chapter names from `.csv` text file
|
54
|
+
(in NUMBER,NAME format, e.g. "1,Intro")
|
55
|
+
--no-log don't write log file
|
56
|
+
--dry-run don't transcode, just show `HandBrakeCLI` command and exit
|
57
|
+
|
58
|
+
Quality options:
|
59
|
+
--big raise default limits for both video and AC-3 audio bitrates
|
60
|
+
(always increases output size)
|
61
|
+
--quick trade some precision for 45-50% increase in encoding speed
|
62
|
+
(more than 15% speedier than x264 encoder "fast" preset)
|
63
|
+
(avoids quality loss of "faster" and "veryfast" presets)
|
64
|
+
--preset veryfast|faster|fast|slow|slower|veryslow
|
65
|
+
apply x264 encoder preset
|
66
|
+
|
67
|
+
Video options:
|
68
|
+
--crop T:B:L:R set video crop values (default: 0:0:0:0)
|
69
|
+
(use `--crop detect` for optimal crop values)
|
70
|
+
(use `--crop auto` for `HandBrakeCLI` behavior)
|
71
|
+
--720p fit video within 1280x720 pixel bounds
|
72
|
+
--max-width WIDTH, --max-height HEIGHT
|
73
|
+
fit video within horizontal and/or vertical pixel bounds
|
74
|
+
--pixel-aspect X:Y
|
75
|
+
set pixel aspect ratio (default: 1:1)
|
76
|
+
(e.g.: make X larger than Y to stretch horizontally)
|
77
|
+
--force-rate FPS
|
78
|
+
force constant video frame rate
|
79
|
+
(`23.976` applied automatically for some inputs)
|
80
|
+
--limit-rate FPS
|
81
|
+
set peak-limited video frame rate
|
82
|
+
(`30` applied automatically for most inputs)
|
83
|
+
--filter NAME[=SETTINGS]
|
84
|
+
apply `HandBrakeCLI` video filter with optional settings
|
85
|
+
(`deinterlace` applied automatically for some inputs)
|
86
|
+
(refer to `HandBrakeCLI --help` for more information)
|
87
|
+
(can be used multiple times)
|
88
|
+
|
89
|
+
Audio options:
|
90
|
+
--main-audio TRACK[=NAME]
|
91
|
+
select main audio track with optional name
|
92
|
+
(default: 1 or first in selected language)
|
93
|
+
(default output can be two audio tracks,
|
94
|
+
both surround and stereo, i.e. "width" is `double`)
|
95
|
+
--add-audio TRACK[=NAME]|language[=CODE[,...]]|all
|
96
|
+
add track selected by number assigning it an optional name
|
97
|
+
or add tracks selected by one or more languages
|
98
|
+
or add all tracks
|
99
|
+
(language is indicated by ISO 639-2 code, e.g.: `eng`)
|
100
|
+
(multiple languages are separated by commas)
|
101
|
+
(default output is single AAC audio track,
|
102
|
+
i.e. "width" is `stereo`)
|
103
|
+
(can be used multiple times)
|
104
|
+
--audio-width TRACK|all=double|surround|stereo
|
105
|
+
set audio output "width" for specific track or all tracks
|
106
|
+
with `double` to allow room for two output tracks
|
107
|
+
with `surround` to allow single surround or stereo track
|
108
|
+
with `stereo` to allow only single stereo track
|
109
|
+
(can be used multiple times)
|
110
|
+
--ac3-bitrate 384|448|640
|
111
|
+
set AC-3 audio bitrate (default: 384)
|
112
|
+
--pass-ac3-bitrate 384|448|640
|
113
|
+
set AC-3 audio pass-through bitrate (default: 448)
|
114
|
+
--copy-audio TRACK|all
|
115
|
+
try to copy track selected by number in its original format
|
116
|
+
falling back to AC-3 format if original not allowed
|
117
|
+
or try to copy all tracks in same manner
|
118
|
+
(can be used multiple times)
|
119
|
+
--copy-audio-name TRACK|all
|
120
|
+
copy original track name selected by number
|
121
|
+
unless the name is specified with another option
|
122
|
+
or try to copy all track names in same manner
|
123
|
+
(can be used multiple times)
|
124
|
+
--no-audio disable all audio output
|
125
|
+
|
126
|
+
Subtitle options:
|
127
|
+
--burn-subtitle TRACK|scan
|
128
|
+
burn track selected by number into video
|
129
|
+
or `scan` to find forced track in main audio language
|
130
|
+
--force-subtitle TRACK|scan
|
131
|
+
add track selected by number and set forced flag
|
132
|
+
or scan for forced track in same language as main audio
|
133
|
+
--add-subtitle TRACK|language[=CODE[,...]]|all
|
134
|
+
add track selected by number
|
135
|
+
or add tracks selected by one or more languages
|
136
|
+
or add all tracks
|
137
|
+
(language is indicated by ISO 639-2 code, e.g.: `eng`)
|
138
|
+
(multiple languages are separated by commas)
|
139
|
+
(can be used multiple times)
|
140
|
+
--no-auto-burn don't automatically burn first forced subtitle
|
141
|
+
|
142
|
+
External subtitle options:
|
143
|
+
--burn-srt FILENAME
|
144
|
+
burn SubRip-format text file into video
|
145
|
+
--force-srt FILENAME
|
146
|
+
add subtitle track from SubRip-format text file
|
147
|
+
and set forced flag
|
148
|
+
--add-srt FILENAME
|
149
|
+
add subtitle track from SubRip-format text file
|
150
|
+
(can be used multiple times)
|
151
|
+
--bind-srt-language CODE
|
152
|
+
bind ISO 639-2 language code (default: und)
|
153
|
+
to previously forced or added subtitle
|
154
|
+
(can be used multiple times)
|
155
|
+
--bind-srt-encoding FORMAT
|
156
|
+
bind character set encoding (default: latin1)
|
157
|
+
to previously burned, forced or added subtitle
|
158
|
+
(can be used multiple times)
|
159
|
+
--bind-srt-offset MILLISECONDS
|
160
|
+
bind +/- offset in milliseconds (default: 0)
|
161
|
+
to previously burned, forced or added subtitle
|
162
|
+
(can be used multiple times)
|
163
|
+
|
164
|
+
Advanced options:
|
165
|
+
-E, --encoder-option NAME=VALUE|_NAME
|
166
|
+
pass x264 video encoder option by name with value
|
167
|
+
or disable use of option by prefixing name with "_"
|
168
|
+
(e.g.: `-E vbv-bufsize=8000`)
|
169
|
+
(e.g.: `-E _crf-max`)
|
170
|
+
(refer to `x264 --fullhelp` for more information)
|
171
|
+
(can be used multiple times)
|
172
|
+
-H, --handbrake-option NAME[=VALUE]|_NAME
|
173
|
+
pass `HandBrakeCLI` option by name or by name with value
|
174
|
+
or disable use of option by prefixing name with "_"
|
175
|
+
(e.g.: `-H stop-at=duration:30`)
|
176
|
+
(e.g.: `-H _markers`)
|
177
|
+
(refer to `HandBrakeCLI --help` for more information)
|
178
|
+
(some options are not allowed)
|
179
|
+
(can be used multiple times)
|
180
|
+
|
181
|
+
Diagnostic options:
|
182
|
+
-v, --verbose increase diagnostic information
|
183
|
+
-q, --quiet decrease " "
|
184
|
+
|
185
|
+
Other options:
|
186
|
+
-h, --help display this help and exit
|
187
|
+
--version output version information and exit
|
188
|
+
|
189
|
+
Requires `HandBrakeCLI`, `mp4track`, `mplayer` and `mkvpropedit`.
|
190
|
+
HERE
|
191
|
+
end
|
192
|
+
|
193
|
+
def initialize
|
194
|
+
super
|
195
|
+
@scan = false
|
196
|
+
@title = nil
|
197
|
+
@output = nil
|
198
|
+
@format = :mkv
|
199
|
+
@log = true
|
200
|
+
@dry_run = false
|
201
|
+
@vbv_maxrate_2160p = 10000
|
202
|
+
@vbv_maxrate_1080p = 5000
|
203
|
+
@vbv_maxrate_720p = 4000
|
204
|
+
@vbv_maxrate_480p = 2000
|
205
|
+
@quick = false
|
206
|
+
@crop = {:top => 0, :bottom => 0, :left => 0, :right => 0}
|
207
|
+
@main_audio = nil
|
208
|
+
@extra_audio = []
|
209
|
+
@audio_name = {}
|
210
|
+
@audio_language = []
|
211
|
+
@audio_width = {:main => :double, :other => :stereo}
|
212
|
+
@ac3_bitrate = 384
|
213
|
+
@pass_ac3_bitrate = 448
|
214
|
+
@copy_audio = []
|
215
|
+
@copy_audio_name = []
|
216
|
+
@burn_subtitle = nil
|
217
|
+
@force_subtitle = nil
|
218
|
+
@extra_subtitle = []
|
219
|
+
@subtitle_language = []
|
220
|
+
@auto_burn = true
|
221
|
+
@burn_srt = nil
|
222
|
+
@force_srt = nil
|
223
|
+
@srt_file = []
|
224
|
+
@srt_language = {}
|
225
|
+
@srt_encoding = {}
|
226
|
+
@srt_offset = {}
|
227
|
+
@encoder_options = {}
|
228
|
+
@disable_encoder_options = []
|
229
|
+
@handbrake_options = {}
|
230
|
+
@disable_handbrake_options = []
|
231
|
+
@temporary = nil
|
232
|
+
end
|
233
|
+
|
234
|
+
def define_options(opts)
|
235
|
+
opts.on('--scan') { @scan = true }
|
236
|
+
opts.on('--title ARG', Integer) { |arg| @title = arg }
|
237
|
+
|
238
|
+
opts.on '--chapters ARG' do |arg|
|
239
|
+
unless arg =~ /^[1-9][0-9]*(?:-[1-9][0-9]*)?$/
|
240
|
+
fail UsageError, "invalid chapters argument: #{arg}"
|
241
|
+
end
|
242
|
+
|
243
|
+
force_handbrake_option 'chapters', arg
|
244
|
+
end
|
245
|
+
|
246
|
+
opts.on '-o', '--output ARG' do |arg|
|
247
|
+
unless File.directory? arg
|
248
|
+
@format = case File.extname(arg)
|
249
|
+
when '.mkv'
|
250
|
+
:mkv
|
251
|
+
when '.mp4'
|
252
|
+
:mp4
|
253
|
+
when '.m4v'
|
254
|
+
:m4v
|
255
|
+
else
|
256
|
+
fail UsageError, "unsupported filename extension: #{arg}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
@output = arg
|
261
|
+
end
|
262
|
+
|
263
|
+
opts.on '--mp4' do
|
264
|
+
@output = filter_output_option(@output, '.mp4')
|
265
|
+
@format = :mp4
|
266
|
+
end
|
267
|
+
|
268
|
+
opts.on '--m4v' do
|
269
|
+
@output = filter_output_option(@output, '.m4v')
|
270
|
+
@format = :m4v
|
271
|
+
end
|
272
|
+
|
273
|
+
opts.on '--chapter-names ARG' do |arg|
|
274
|
+
fail "chapter names file does not exist: #{arg}" unless File.exist? arg
|
275
|
+
force_handbrake_option 'markers', arg
|
276
|
+
end
|
277
|
+
|
278
|
+
opts.on('--no-log') { @log = false }
|
279
|
+
opts.on('--dry-run') { @dry_run = true }
|
280
|
+
|
281
|
+
opts.on '--big' do
|
282
|
+
@vbv_maxrate_2160p = 16000
|
283
|
+
@vbv_maxrate_1080p = 8000
|
284
|
+
@vbv_maxrate_720p = 6000
|
285
|
+
@vbv_maxrate_480p = 3000
|
286
|
+
@ac3_bitrate = 640
|
287
|
+
@pass_ac3_bitrate = 640
|
288
|
+
end
|
289
|
+
|
290
|
+
opts.on '--quick' do
|
291
|
+
@quick = true
|
292
|
+
@handbrake_options.delete 'encoder-preset'
|
293
|
+
end
|
294
|
+
|
295
|
+
opts.on '--preset ARG' do |arg|
|
296
|
+
case arg
|
297
|
+
when 'medium'
|
298
|
+
@handbrake_options.delete 'encoder-preset'
|
299
|
+
when 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'slow',
|
300
|
+
'slower', 'veryslow', 'placebo'
|
301
|
+
force_handbrake_option 'encoder-preset', arg
|
302
|
+
else
|
303
|
+
fail UsageError, "unsupported preset name: #{arg}"
|
304
|
+
end
|
305
|
+
|
306
|
+
@quick = false
|
307
|
+
end
|
308
|
+
|
309
|
+
opts.on '--crop ARG' do |arg|
|
310
|
+
@crop = case arg
|
311
|
+
when /^([0-9]+):([0-9]+):([0-9]+):([0-9]+)$/
|
312
|
+
{:top => $1.to_i, :bottom => $2.to_i, :left => $3.to_i, :right => $4.to_i}
|
313
|
+
when 'detect'
|
314
|
+
:detect
|
315
|
+
when 'auto'
|
316
|
+
:auto
|
317
|
+
else
|
318
|
+
fail UsageError, "invalid crop values: #{arg}"
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
opts.on '--720p' do
|
323
|
+
force_handbrake_option 'maxWidth', '1280'
|
324
|
+
force_handbrake_option 'maxHeight', '720'
|
325
|
+
@handbrake_options.delete 'width'
|
326
|
+
@handbrake_options.delete 'height'
|
327
|
+
end
|
328
|
+
|
329
|
+
opts.on '--max-width ARG', Integer do |arg|
|
330
|
+
fail UsageError, "invalid maximum width argument: #{arg}" if arg < 0 or arg > MAX_WIDTH
|
331
|
+
force_handbrake_option 'maxWidth', arg.to_s
|
332
|
+
@handbrake_options.delete 'width'
|
333
|
+
end
|
334
|
+
|
335
|
+
opts.on '--max-height ARG', Integer do |arg|
|
336
|
+
fail UsageError, "invalid maximum height argument: #{arg}" if arg < 0 or arg > MAX_HEIGHT
|
337
|
+
force_handbrake_option 'maxHeight', arg.to_s
|
338
|
+
@handbrake_options.delete 'height'
|
339
|
+
end
|
340
|
+
|
341
|
+
opts.on '--pixel-aspect ARG' do |arg|
|
342
|
+
if arg =~ /^[1-9][0-9]*:[1-9][0-9]*$/
|
343
|
+
force_handbrake_option 'pixel-aspect', arg
|
344
|
+
force_handbrake_option 'custom-anamorphic', nil
|
345
|
+
@handbrake_options.delete 'display-width'
|
346
|
+
@handbrake_options.delete 'strict-anamorphic'
|
347
|
+
@handbrake_options.delete 'loose-anamorphic'
|
348
|
+
else
|
349
|
+
fail UsageError, "invalid pixel aspect argument: #{arg}"
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
opts.on '--force-rate ARG' do |arg|
|
354
|
+
unless arg =~ /^[1-9][0-9]*(?:\.[0-9]+)?$/
|
355
|
+
fail UsageError, "invalid force rate argument: #{arg}"
|
356
|
+
end
|
357
|
+
|
358
|
+
force_handbrake_option 'rate', arg
|
359
|
+
@handbrake_options.delete 'vfr'
|
360
|
+
@handbrake_options.delete 'pfr'
|
361
|
+
end
|
362
|
+
|
363
|
+
opts.on '--limit-rate ARG' do |arg|
|
364
|
+
unless arg =~ /^[1-9][0-9]*(?:\.[0-9]+)?$/
|
365
|
+
fail UsageError, "invalid limit rate argument: #{arg}"
|
366
|
+
end
|
367
|
+
|
368
|
+
force_handbrake_option 'rate', arg
|
369
|
+
force_handbrake_option 'pfr', nil
|
370
|
+
@handbrake_options.delete 'vfr'
|
371
|
+
@handbrake_options.delete 'cfr'
|
372
|
+
end
|
373
|
+
|
374
|
+
opts.on '--filter ARG' do |arg|
|
375
|
+
if arg =~ /^([a-z]+)(?:=(.+))?$/
|
376
|
+
case $1
|
377
|
+
when 'deinterlace', 'decomb', 'detelecine', 'denoise', 'nlmeans',
|
378
|
+
'nlmeans-tune', 'deblock', 'rotate', 'grayscale'
|
379
|
+
force_handbrake_option $1, $2
|
380
|
+
else
|
381
|
+
fail UsageError, "unsupported filter name: #{$1}"
|
382
|
+
end
|
383
|
+
else
|
384
|
+
fail UsageError, "invalid filter argument: #{arg}"
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
opts.on '--main-audio ARG' do |arg|
|
389
|
+
if arg =~ /^([1-9][0-9]*)(?:=(.+))?$/
|
390
|
+
track = $1.to_i
|
391
|
+
@main_audio = track
|
392
|
+
@audio_name[track] = $2 unless $2.nil?
|
393
|
+
else
|
394
|
+
fail UsageError, "invalid main audio argument: #{arg}"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
opts.on '--add-audio ARG' do |arg|
|
399
|
+
if arg =~ /^(?:([1-9][0-9]*)(?:=(.+))?|lang(?:uage)?=([a-z]{3}(?:,[a-z]{3})*)|(all))$/
|
400
|
+
if $4.nil?
|
401
|
+
if $3.nil?
|
402
|
+
track = $1.to_i
|
403
|
+
@extra_audio << track unless @extra_audio.first.is_a? Symbol
|
404
|
+
@audio_name[track] = $2 unless $2.nil?
|
405
|
+
elsif @extra_audio.first != :all
|
406
|
+
@extra_audio = [:language]
|
407
|
+
@audio_language = $3.split(',')
|
408
|
+
end
|
409
|
+
else
|
410
|
+
@extra_audio = [:all]
|
411
|
+
@audio_language = []
|
412
|
+
end
|
413
|
+
else
|
414
|
+
fail UsageError, "invalid add audio argument: #{arg}"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
opts.on '--audio-width ARG' do |arg|
|
419
|
+
if arg =~ /^([1-9][0-9]*|all)=(double|surround|stereo)$/
|
420
|
+
width = $2.to_sym
|
421
|
+
|
422
|
+
if $1 == 'all'
|
423
|
+
@audio_width[:main] = width
|
424
|
+
@audio_width[:other] = width
|
425
|
+
else
|
426
|
+
@audio_width[$1.to_i] = width
|
427
|
+
end
|
428
|
+
else
|
429
|
+
fail UsageError, "invalid audio width argument: #{arg}"
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
opts.on '--ac3-bitrate ARG', Integer do |arg|
|
434
|
+
@ac3_bitrate = case arg
|
435
|
+
when 384, 448, 640
|
436
|
+
arg
|
437
|
+
else
|
438
|
+
fail UsageError, "unsupported AC-3 audio bitrate: #{arg}"
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
opts.on '--pass-ac3-bitrate ARG', Integer do |arg|
|
443
|
+
@pass_ac3_bitrate = case arg
|
444
|
+
when 384, 448, 640
|
445
|
+
arg
|
446
|
+
else
|
447
|
+
fail UsageError, "unsupported AC-3 audio pass-through bitrate: #{arg}"
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
opts.on '--copy-audio ARG' do |arg|
|
452
|
+
if arg =~ /^[1-9][0-9]*|all$/
|
453
|
+
if $MATCH == 'all'
|
454
|
+
@copy_audio = [:all]
|
455
|
+
else
|
456
|
+
@copy_audio << $MATCH.to_i unless @copy_audio.first == :all
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
opts.on '--copy-audio-name ARG' do |arg|
|
462
|
+
if arg =~ /^[1-9][0-9]*|all$/
|
463
|
+
if $MATCH == 'all'
|
464
|
+
@copy_audio_name = [:all]
|
465
|
+
else
|
466
|
+
@copy_audio_name << $MATCH.to_i unless @copy_audio_name.first == :all
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
opts.on('--no-audio') { force_handbrake_option 'audio', 'none' }
|
472
|
+
|
473
|
+
opts.on '--burn-subtitle ARG' do |arg|
|
474
|
+
if arg =~ /^[1-9][0-9]*|scan$/
|
475
|
+
if $MATCH == 'scan'
|
476
|
+
@burn_subtitle = :scan
|
477
|
+
else
|
478
|
+
track = $MATCH.to_i
|
479
|
+
@burn_subtitle = track
|
480
|
+
@extra_subtitle << track
|
481
|
+
end
|
482
|
+
|
483
|
+
@force_subtitle = nil
|
484
|
+
@burn_srt = nil
|
485
|
+
@force_srt = nil
|
486
|
+
@auto_burn = false
|
487
|
+
else
|
488
|
+
fail UsageError, "invalid burn subtitle argument: #{arg}"
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
opts.on '--force-subtitle ARG' do |arg|
|
493
|
+
if arg =~ /^[1-9][0-9]*|scan$/
|
494
|
+
if $MATCH == 'scan'
|
495
|
+
@force_subtitle = :scan
|
496
|
+
else
|
497
|
+
track = $MATCH.to_i
|
498
|
+
@force_subtitle = track
|
499
|
+
@extra_subtitle << track
|
500
|
+
end
|
501
|
+
|
502
|
+
@burn_subtitle = nil
|
503
|
+
@burn_srt = nil
|
504
|
+
@force_srt = nil
|
505
|
+
@auto_burn = false
|
506
|
+
else
|
507
|
+
fail UsageError, "invalid force subtitle argument: #{arg}"
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
opts.on '--add-subtitle ARG' do |arg|
|
512
|
+
if arg =~ /^(?:([1-9][0-9]*)|lang(?:uage)?=([a-z]{3}(?:,[a-z]{3})*)|(all))$/
|
513
|
+
if $3.nil?
|
514
|
+
if $2.nil?
|
515
|
+
unless @extra_subtitle.first.is_a? Symbol
|
516
|
+
@extra_subtitle << $1.to_i
|
517
|
+
end
|
518
|
+
elsif @extra_subtitle.first != :all
|
519
|
+
@extra_subtitle = [:language]
|
520
|
+
@subtitle_language = $2.split(',')
|
521
|
+
end
|
522
|
+
else
|
523
|
+
@extra_subtitle = [:all]
|
524
|
+
@subtitle_language = []
|
525
|
+
end
|
526
|
+
else
|
527
|
+
fail UsageError, "invalid add subtitle argument: #{arg}"
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
opts.on('--no-auto-burn') { @auto_burn = false }
|
532
|
+
|
533
|
+
opts.on '--burn-srt ARG' do |arg|
|
534
|
+
fail "subtitle file does not exist: #{arg}" unless File.exist? arg
|
535
|
+
index = @srt_file.index(arg)
|
536
|
+
|
537
|
+
if index.nil?
|
538
|
+
@burn_srt = @srt_file.size
|
539
|
+
@srt_file << arg
|
540
|
+
else
|
541
|
+
@burn_srt = index
|
542
|
+
end
|
543
|
+
|
544
|
+
@force_srt = nil
|
545
|
+
@burn_subtitle = nil
|
546
|
+
@force_subtitle = nil
|
547
|
+
@auto_burn = false
|
548
|
+
end
|
549
|
+
|
550
|
+
opts.on '--force-srt ARG' do |arg|
|
551
|
+
fail "subtitle file does not exist: #{arg}" unless File.exist? arg
|
552
|
+
index = @srt_file.index(arg)
|
553
|
+
|
554
|
+
if index.nil?
|
555
|
+
@force_srt = @srt_file.size
|
556
|
+
@srt_file << arg
|
557
|
+
else
|
558
|
+
@force_srt = index
|
559
|
+
end
|
560
|
+
|
561
|
+
@burn_srt = nil
|
562
|
+
@burn_subtitle = nil
|
563
|
+
@force_subtitle = nil
|
564
|
+
@auto_burn = false
|
565
|
+
end
|
566
|
+
|
567
|
+
opts.on '--add-srt ARG' do |arg|
|
568
|
+
fail "subtitle file does not exist: #{arg}" unless File.exist? arg
|
569
|
+
@srt_file << arg unless @srt_file.include? arg
|
570
|
+
end
|
571
|
+
|
572
|
+
opts.on '--bind-srt-language ARG' do |arg|
|
573
|
+
fail UsageError, "invalid subtitle language argument: #{arg}" unless arg =~ /^[a-z]{3}$/
|
574
|
+
fail UsageError, "subtitle file missing for language: #{arg}" if @srt_file.empty?
|
575
|
+
@srt_language[@srt_file.size - 1] = arg
|
576
|
+
end
|
577
|
+
|
578
|
+
opts.on '--bind-srt-encoding ARG' do |arg|
|
579
|
+
fail UsageError, "subtitle file missing for encoding: #{arg}" if @srt_file.empty?
|
580
|
+
@srt_encoding[@srt_file.size - 1] = arg
|
581
|
+
end
|
582
|
+
|
583
|
+
opts.on '--bind-srt-offset ARG', Integer do |arg|
|
584
|
+
fail UsageError, "subtitle file missing for offset: #{arg}" if @srt_file.empty?
|
585
|
+
@srt_offset[@srt_file.size - 1] = arg
|
586
|
+
end
|
587
|
+
|
588
|
+
opts.on '-E', '--encoder-option ARG' do |arg|
|
589
|
+
if arg =~ /^([a-z][a-z_-]+)=([^ :]+)$/
|
590
|
+
@encoder_options[$1] = $2
|
591
|
+
@disable_encoder_options.delete $1
|
592
|
+
elsif arg =~ /^_([a-z][a-z_-]+)$/
|
593
|
+
@disable_encoder_options << $1
|
594
|
+
@encoder_options.delete $1
|
595
|
+
else
|
596
|
+
fail UsageError, "invalid encoder option: #{arg}"
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
opts.on '-H', '--handbrake-option ARG' do |arg|
|
601
|
+
if arg =~ /^([a-zA-Z][a-zA-Z0-9-]+)(?:=(.+))?$/
|
602
|
+
force_handbrake_option filter_handbrake_option($1), $2
|
603
|
+
elsif arg =~ /^_([a-zA-Z][a-zA-Z0-9-]+)$/
|
604
|
+
name = filter_handbrake_option($1)
|
605
|
+
@disable_handbrake_options << name
|
606
|
+
@handbrake_options.delete name
|
607
|
+
else
|
608
|
+
fail UsageError, "invalid HandBrakeCLI option: #{arg}"
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
def force_handbrake_option(name, value)
|
614
|
+
@handbrake_options[name] = value
|
615
|
+
@disable_handbrake_options.delete name
|
616
|
+
end
|
617
|
+
|
618
|
+
def filter_output_option(path, ext)
|
619
|
+
if path.nil? or File.directory? path
|
620
|
+
path
|
621
|
+
else
|
622
|
+
File.basename(path, '.*') + ext
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def filter_handbrake_option(name)
|
627
|
+
case name
|
628
|
+
when 'help', 'update', 'preset', 'preset-list', 'input', 'title',
|
629
|
+
'scan', 'main-feature', 'previews', 'output', 'format',
|
630
|
+
'encoder-preset-list', 'encoder-tune-list', 'encoder-profile-list',
|
631
|
+
'encoder-level-list'
|
632
|
+
fail UsageError, "unsupported HandBrakeCLI option name: #{name}"
|
633
|
+
when 'qsv-preset', 'x264-preset', 'x265-preset'
|
634
|
+
'encoder-preset'
|
635
|
+
when 'x264-tune', 'x265-tune'
|
636
|
+
'encoder-tune'
|
637
|
+
when 'x264-profile', 'h264-profile', 'h265-profile'
|
638
|
+
'encoder-profile'
|
639
|
+
when 'h264-level', 'h265-level'
|
640
|
+
'encoder-level'
|
641
|
+
else
|
642
|
+
name
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
def configure
|
647
|
+
@extra_audio.uniq!
|
648
|
+
@copy_audio.uniq!
|
649
|
+
@copy_audio_name.uniq!
|
650
|
+
@extra_subtitle.uniq!
|
651
|
+
@disable_encoder_options.uniq!
|
652
|
+
@disable_handbrake_options.uniq!
|
653
|
+
HandBrake.setup
|
654
|
+
MP4track.setup
|
655
|
+
MPlayer.setup
|
656
|
+
MKVpropedit.setup
|
657
|
+
end
|
658
|
+
|
659
|
+
def process_input(arg)
|
660
|
+
Console.info "Processing: #{arg}..."
|
661
|
+
|
662
|
+
if @scan
|
663
|
+
media = Media.new(path: arg, title: @title)
|
664
|
+
Console.debug media.info
|
665
|
+
puts media.summary
|
666
|
+
return
|
667
|
+
end
|
668
|
+
|
669
|
+
seconds = Time.now.tv_sec
|
670
|
+
media = Media.new(path: arg, title: @title, autocrop: @crop == :detect)
|
671
|
+
Console.debug media.info
|
672
|
+
handbrake_options = {
|
673
|
+
'input' => arg,
|
674
|
+
'output' => resolve_output(media),
|
675
|
+
'markers' => nil,
|
676
|
+
'encoder' => 'x264',
|
677
|
+
'quality' => '16'
|
678
|
+
}
|
679
|
+
title = media.info[:title]
|
680
|
+
handbrake_options['title'] = title.to_s unless title == 1
|
681
|
+
encoder_options = {}
|
682
|
+
prepare_video(media, handbrake_options, encoder_options)
|
683
|
+
renamed_audio = {}
|
684
|
+
prepare_audio(media, handbrake_options, renamed_audio)
|
685
|
+
prepare_subtitle(media, handbrake_options)
|
686
|
+
prepare_srt(media, handbrake_options)
|
687
|
+
prepare_options(handbrake_options, encoder_options)
|
688
|
+
transcode(handbrake_options)
|
689
|
+
adjust_metadata(handbrake_options['output'], renamed_audio)
|
690
|
+
|
691
|
+
unless @dry_run
|
692
|
+
seconds = Time.now.tv_sec - seconds
|
693
|
+
hours = seconds / (60 * 60)
|
694
|
+
minutes = (seconds / 60) % 60
|
695
|
+
seconds = seconds % 60
|
696
|
+
printf "Elapsed time: %02d:%02d:%02d\n\n", hours, minutes, seconds
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
def resolve_output(media)
|
701
|
+
output = File.basename(media.path, '.*') + '.' + @format.to_s
|
702
|
+
|
703
|
+
unless @output.nil?
|
704
|
+
path = File.absolute_path(@output)
|
705
|
+
|
706
|
+
if File.directory? @output
|
707
|
+
output = path + File::SEPARATOR + output
|
708
|
+
else
|
709
|
+
output = path
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
fail "output file exists: #{media.path}" if File.exist? output
|
714
|
+
output
|
715
|
+
end
|
716
|
+
|
717
|
+
def prepare_video(media, handbrake_options, encoder_options)
|
718
|
+
crop = resolve_crop(media)
|
719
|
+
width, height = media.info[:width], media.info[:height]
|
720
|
+
|
721
|
+
unless crop.nil?
|
722
|
+
handbrake_options['crop'] = Crop.handbrake_string(crop)
|
723
|
+
width -= crop[:left] + crop[:right]
|
724
|
+
height -= crop[:top] + crop[:bottom]
|
725
|
+
|
726
|
+
unless width > 0 and height > 0
|
727
|
+
fail UsageError, "invalid crop values: #{Crop.handbrake_string(crop)}"
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
width = @handbrake_options.fetch('width', width).to_i
|
732
|
+
height = @handbrake_options.fetch('height', height).to_i
|
733
|
+
max_width = @handbrake_options.fetch('maxWidth', MAX_WIDTH).to_i
|
734
|
+
max_height = @handbrake_options.fetch('maxHeight', MAX_HEIGHT).to_i
|
735
|
+
|
736
|
+
if width > max_width or height > max_height
|
737
|
+
anamorphic = 'loose-anamorphic'
|
738
|
+
adjusted_height = (height * (max_width.to_f / width)).to_i
|
739
|
+
adjusted_height -= 1 if adjusted_height.odd?
|
740
|
+
|
741
|
+
if adjusted_height > max_height
|
742
|
+
width = (width * (max_height.to_f / height)).to_i
|
743
|
+
width -= 1 if width.odd?
|
744
|
+
height = max_height
|
745
|
+
else
|
746
|
+
width = max_width
|
747
|
+
height = adjusted_height
|
748
|
+
end
|
749
|
+
else
|
750
|
+
anamorphic = 'strict-anamorphic'
|
751
|
+
end
|
752
|
+
|
753
|
+
handbrake_options[anamorphic] = nil unless @handbrake_options.has_key? 'custom-anamorphic'
|
754
|
+
preset = @handbrake_options.fetch('encoder-preset', 'medium')
|
755
|
+
|
756
|
+
if width > 1920 or height > 1080
|
757
|
+
vbv_maxrate = @vbv_maxrate_2160p
|
758
|
+
max_bufsize = 300000
|
759
|
+
elsif width > 1280 or height > 720
|
760
|
+
vbv_maxrate = @vbv_maxrate_1080p
|
761
|
+
max_bufsize = 25000
|
762
|
+
|
763
|
+
case preset
|
764
|
+
when 'slow', 'slower', 'veryslow', 'placebo'
|
765
|
+
handbrake_options['encoder-level'] = '4.0'
|
766
|
+
end
|
767
|
+
elsif width > 720 or height > 576
|
768
|
+
vbv_maxrate = @vbv_maxrate_720p
|
769
|
+
max_bufsize = 17500
|
770
|
+
else
|
771
|
+
vbv_maxrate = @vbv_maxrate_480p
|
772
|
+
max_bufsize = 12500
|
773
|
+
end
|
774
|
+
|
775
|
+
unless media.info[:directory]
|
776
|
+
bitrate = ((((media.info[:size] * 8) / media.info[:duration]) / 1000) / 1000) * 1000
|
777
|
+
|
778
|
+
if bitrate < vbv_maxrate
|
779
|
+
min_bitrate = vbv_maxrate / 2
|
780
|
+
|
781
|
+
if bitrate < min_bitrate
|
782
|
+
vbv_maxrate = min_bitrate
|
783
|
+
else
|
784
|
+
vbv_maxrate = bitrate
|
785
|
+
end
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
vbv_bufsize = @encoder_options.fetch('vbv-maxrate', vbv_maxrate).to_i / 2
|
790
|
+
vbv_bufsize = max_bufsize if vbv_bufsize > max_bufsize
|
791
|
+
|
792
|
+
case preset
|
793
|
+
when 'slower', 'veryslow', 'placebo'
|
794
|
+
encoder_options['ref'] = '5'
|
795
|
+
end
|
796
|
+
|
797
|
+
encoder_options['vbv-maxrate'] = vbv_maxrate.to_s
|
798
|
+
encoder_options['vbv-bufsize'] = (vbv_maxrate / 2).to_s
|
799
|
+
encoder_options['crf-max'] = '25'
|
800
|
+
|
801
|
+
if @quick and not @handbrake_options.has_key? 'encoder-preset'
|
802
|
+
encoder_options['ref'] = '1'
|
803
|
+
encoder_options['weightp'] = '1'
|
804
|
+
encoder_options['subme'] = '6'
|
805
|
+
encoder_options['mixed-refs'] = '0'
|
806
|
+
encoder_options['rc-lookahead'] = '30'
|
807
|
+
end
|
808
|
+
|
809
|
+
unless @handbrake_options.has_key? 'rate'
|
810
|
+
fps = media.info[:fps]
|
811
|
+
|
812
|
+
if fps == 29.97
|
813
|
+
handbrake_options['rate'] = '23.976'
|
814
|
+
|
815
|
+
unless @handbrake_options.has_key? 'deinterlace' or
|
816
|
+
@handbrake_options.has_key? 'decomb' or
|
817
|
+
@handbrake_options.has_key? 'detelecine'
|
818
|
+
handbrake_options['deinterlace'] = nil
|
819
|
+
end
|
820
|
+
elsif media.info[:mpeg2]
|
821
|
+
case fps
|
822
|
+
when 23.976, 24.0, 25.0
|
823
|
+
handbrake_options['rate'] = fps.to_s
|
824
|
+
end
|
825
|
+
else
|
826
|
+
handbrake_options['rate'] = '30'
|
827
|
+
handbrake_options['pfr'] = nil
|
828
|
+
end
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
def resolve_crop(media)
|
833
|
+
if @crop == :detect
|
834
|
+
width, height = media.info[:width], media.info[:height]
|
835
|
+
hb_crop = Crop.constrain(media.info[:autocrop], width, height)
|
836
|
+
|
837
|
+
unless media.info[:directory]
|
838
|
+
mp_crop = Crop.detect(media.path, media.info[:duration], width, height)
|
839
|
+
mp_crop = Crop.constrain(mp_crop, width, height)
|
840
|
+
|
841
|
+
if hb_crop != mp_crop
|
842
|
+
Console.error 'Results differ...'
|
843
|
+
Console.error "From HandBrakeCLI: #{Crop.handbrake_string(hb_crop)}"
|
844
|
+
Console.error "From mplayer: #{Crop.handbrake_string(mp_crop)}"
|
845
|
+
fail "crop detection failed: #{media.path}"
|
846
|
+
end
|
847
|
+
end
|
848
|
+
|
849
|
+
hb_crop
|
850
|
+
elsif @crop == :auto
|
851
|
+
nil
|
852
|
+
else
|
853
|
+
@crop
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
def prepare_audio(media, handbrake_options, renamed_audio)
|
858
|
+
return if @handbrake_options.fetch('audio', '') == 'none' or media.info[:audio].empty?
|
859
|
+
main_track = resolve_main_audio(media)
|
860
|
+
@audio_width[main_track] ||= @audio_width[:main]
|
861
|
+
track_order = [main_track]
|
862
|
+
|
863
|
+
case @extra_audio.first
|
864
|
+
when :all
|
865
|
+
media.info[:audio].each { |track, _| track_order << track unless track == main_track }
|
866
|
+
when :language
|
867
|
+
media.info[:audio].each do |track, info|
|
868
|
+
if track != main_track and @audio_language.include? info[:language]
|
869
|
+
track_order << track
|
870
|
+
end
|
871
|
+
end
|
872
|
+
else
|
873
|
+
@extra_audio.each do |track|
|
874
|
+
track_order << track if track != main_track and media.info[:audio].include? track
|
875
|
+
end
|
876
|
+
end
|
877
|
+
|
878
|
+
tracks, encoders, bitrates, names = [], [], [], []
|
879
|
+
|
880
|
+
add_surround = ->(info, copy) do
|
881
|
+
bitrate = info[:bps].nil? ? 640 : info[:bps] / 1000
|
882
|
+
|
883
|
+
if copy or (info[:format] == 'AC3' and bitrate <= @pass_ac3_bitrate)
|
884
|
+
encoders << 'copy'
|
885
|
+
bitrates << ''
|
886
|
+
else
|
887
|
+
encoders << 'ac3'
|
888
|
+
|
889
|
+
if @ac3_bitrate == 640
|
890
|
+
bitrates << ''
|
891
|
+
else
|
892
|
+
bitrates << @ac3_bitrate.to_s
|
893
|
+
end
|
894
|
+
end
|
895
|
+
end
|
896
|
+
|
897
|
+
add_stereo = ->(info, copy) do
|
898
|
+
if copy or (info[:format] == 'AAC' and info[:channels] <= 2.0)
|
899
|
+
encoders << 'copy'
|
900
|
+
else
|
901
|
+
encoders << HandBrake.aac_encoder
|
902
|
+
end
|
903
|
+
|
904
|
+
bitrates << ''
|
905
|
+
end
|
906
|
+
|
907
|
+
track_order.each do |track|
|
908
|
+
tracks << track
|
909
|
+
info = media.info[:audio][track]
|
910
|
+
copy = (@copy_audio.first == :all or @copy_audio.include? track)
|
911
|
+
|
912
|
+
if @copy_audio_name.first == :all or @copy_audio_name.include? track
|
913
|
+
name = info.fetch(:name, '')
|
914
|
+
else
|
915
|
+
name = ''
|
916
|
+
end
|
917
|
+
|
918
|
+
name = @audio_name.fetch(track, name)
|
919
|
+
|
920
|
+
if name =~ /,/
|
921
|
+
sanitized_name = name.gsub(/,/, '_')
|
922
|
+
renamed_audio[sanitized_name] = name
|
923
|
+
name = sanitized_name
|
924
|
+
end
|
925
|
+
|
926
|
+
names << name
|
927
|
+
|
928
|
+
case @audio_width.fetch(track, @audio_width[:other])
|
929
|
+
when :double
|
930
|
+
if info[:channels] > 2.0
|
931
|
+
tracks << track
|
932
|
+
names << name
|
933
|
+
|
934
|
+
if @format == :mkv
|
935
|
+
add_surround.call info, copy
|
936
|
+
add_stereo.call info, false
|
937
|
+
else
|
938
|
+
add_stereo.call info, false
|
939
|
+
add_surround.call info, copy
|
940
|
+
end
|
941
|
+
else
|
942
|
+
add_stereo.call info, copy
|
943
|
+
end
|
944
|
+
when :surround
|
945
|
+
if info[:channels] > 2.0
|
946
|
+
add_surround.call info, copy
|
947
|
+
else
|
948
|
+
add_stereo.call info, copy
|
949
|
+
end
|
950
|
+
when :stereo
|
951
|
+
add_stereo.call info, copy and info[:channels] <= 2.0
|
952
|
+
end
|
953
|
+
end
|
954
|
+
|
955
|
+
handbrake_options['audio'] = tracks.join(',')
|
956
|
+
handbrake_options['aencoder'] = encoders.join(',')
|
957
|
+
handbrake_options['audio-fallback'] = 'ac3' unless @copy_audio.empty?
|
958
|
+
bitrates = bitrates.join(',')
|
959
|
+
handbrake_options['ab'] = bitrates if bitrates.gsub(/,/, '') != ''
|
960
|
+
names = names.join(',')
|
961
|
+
handbrake_options['aname'] = names if names.gsub(/,/, '') != ''
|
962
|
+
end
|
963
|
+
|
964
|
+
def resolve_main_audio(media)
|
965
|
+
track = @main_audio
|
966
|
+
|
967
|
+
if track.nil?
|
968
|
+
unless @audio_language.empty?
|
969
|
+
track, _ = media.info[:audio].find do |_, info|
|
970
|
+
@audio_language.include? info[:language]
|
971
|
+
end
|
972
|
+
end
|
973
|
+
|
974
|
+
track ||= 1
|
975
|
+
end
|
976
|
+
end
|
977
|
+
|
978
|
+
def prepare_subtitle(media, handbrake_options)
|
979
|
+
return if media.info[:subtitle].empty?
|
980
|
+
|
981
|
+
if @auto_burn
|
982
|
+
burn_track, _ = media.info[:subtitle].find { |_, info| info[:forced] }
|
983
|
+
else
|
984
|
+
burn_track = @burn_subtitle
|
985
|
+
end
|
986
|
+
|
987
|
+
if burn_track == :scan or @force_subtitle == :scan
|
988
|
+
track_order = ['scan']
|
989
|
+
else
|
990
|
+
track_order = []
|
991
|
+
track_order << burn_track.to_s unless burn_track.nil?
|
992
|
+
track_order << @force_subtitle.to_s unless @force_subtitle.nil?
|
993
|
+
end
|
994
|
+
|
995
|
+
case @extra_subtitle.first
|
996
|
+
when :all
|
997
|
+
media.info[:subtitle].each do |track, _|
|
998
|
+
track_order << track unless track == burn_track or track == @force_subtitle
|
999
|
+
end
|
1000
|
+
when :language
|
1001
|
+
media.info[:subtitle].each do |track, info|
|
1002
|
+
unless track == burn_track or track == @force_subtitle
|
1003
|
+
track_order << track if @subtitle_language.include? info[:language]
|
1004
|
+
end
|
1005
|
+
end
|
1006
|
+
else
|
1007
|
+
@extra_subtitle.each do |track|
|
1008
|
+
unless track == burn_track or track == @force_subtitle
|
1009
|
+
track_order << track if media.info[:subtitle].include? track
|
1010
|
+
end
|
1011
|
+
end
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
unless track_order.empty?
|
1015
|
+
track_order = track_order.join(',')
|
1016
|
+
handbrake_options['subtitle'] = track_order if track_order.gsub(/,/, '') != ''
|
1017
|
+
handbrake_options['subtitle-burned'] = nil unless burn_track.nil?
|
1018
|
+
handbrake_options['subtitle-default'] = nil unless @force_subtitle.nil?
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
def prepare_srt(media, handbrake_options)
|
1023
|
+
files, encodings, offsets, languages = [], [], [], []
|
1024
|
+
|
1025
|
+
@srt_file.each_with_index do |file, index|
|
1026
|
+
if file =~ /,/
|
1027
|
+
@temporary ||= Dir.mktmpdir
|
1028
|
+
link = @temporary + File::SEPARATOR + "subtitle_#{media.hash}_#{index}.srt"
|
1029
|
+
File.symlink File.absolute_path(file), link
|
1030
|
+
file = link
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
encoding = @srt_encoding.fetch(index, '')
|
1034
|
+
offset = @srt_offset.fetch(index, '').to_s
|
1035
|
+
language = @srt_language.fetch(index, '')
|
1036
|
+
|
1037
|
+
if index > 0 and (index == @burn_srt or index == @force_srt)
|
1038
|
+
files.unshift file
|
1039
|
+
encodings.unshift encoding
|
1040
|
+
offsets.unshift offset
|
1041
|
+
languages.unshift language
|
1042
|
+
else
|
1043
|
+
files << file
|
1044
|
+
encodings << encoding
|
1045
|
+
offsets << language
|
1046
|
+
languages << language
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
unless files.empty?
|
1051
|
+
files = files.join(',')
|
1052
|
+
handbrake_options['srt-file'] = files
|
1053
|
+
encodings = encodings.join(',')
|
1054
|
+
handbrake_options['srt-codeset'] = encodings if encodings.gsub(/,/, '') != ''
|
1055
|
+
offsets = offsets.join(',')
|
1056
|
+
handbrake_options['srt-offset'] = offsets if offsets.gsub(/,/, '') != ''
|
1057
|
+
languages = languages.join(',')
|
1058
|
+
handbrake_options['srt-lang'] = languages if languages.gsub(/,/, '') != ''
|
1059
|
+
handbrake_options['srt-burn'] = nil unless @burn_srt.nil?
|
1060
|
+
handbrake_options['srt-default'] = nil unless @force_srt.nil?
|
1061
|
+
end
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def prepare_options(handbrake_options, encoder_options)
|
1065
|
+
encoder_options.merge! @encoder_options
|
1066
|
+
@disable_encoder_options.each { |name| encoder_options.delete name }
|
1067
|
+
|
1068
|
+
unless encoder_options.empty?
|
1069
|
+
encopts = ''
|
1070
|
+
encoder_options.each { |name, value| encopts += "#{name}=#{value}:" }
|
1071
|
+
handbrake_options['encopts'] = encopts.chop
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
handbrake_options.merge! @handbrake_options
|
1075
|
+
@disable_handbrake_options.each { |name| handbrake_options.delete name }
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
def transcode(handbrake_options)
|
1079
|
+
handbrake_command = [HandBrake.command_name]
|
1080
|
+
|
1081
|
+
handbrake_options.each do |name, value|
|
1082
|
+
if value.nil?
|
1083
|
+
handbrake_command << "--#{name}"
|
1084
|
+
elsif @dry_run and name != 'encopts'
|
1085
|
+
handbrake_command << "--#{name}=#{value.shellescape}"
|
1086
|
+
else
|
1087
|
+
handbrake_command << "--#{name}=#{value}"
|
1088
|
+
end
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
if @dry_run
|
1092
|
+
puts handbrake_command.join(' ')
|
1093
|
+
return
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
Console.debug handbrake_command
|
1097
|
+
log_file = @log ? File.new(handbrake_options['output'] + '.log', 'w') : nil
|
1098
|
+
Console.info 'Transcoding with HandBrakeCLI...'
|
1099
|
+
|
1100
|
+
begin
|
1101
|
+
IO.popen(handbrake_command, :err=>[:child, :out]) do |io|
|
1102
|
+
Signal.trap 'INT' do
|
1103
|
+
Process.kill 'INT', io.pid
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
io.each_char do |char|
|
1107
|
+
print char
|
1108
|
+
log_file.print char unless log_file.nil?
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
rescue SystemCallError => e
|
1112
|
+
raise "transcoding failed: #{e}"
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
log_file.close unless log_file.nil?
|
1116
|
+
fail "transcoding failed: #{handbrake_options['input']}" unless $CHILD_STATUS.exitstatus == 0
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def adjust_metadata(output, renamed_audio)
|
1120
|
+
return if @dry_run
|
1121
|
+
media = Media.new(path: output, allow_directory: false)
|
1122
|
+
Console.debug media.info
|
1123
|
+
|
1124
|
+
if media.info[:mkv] and
|
1125
|
+
media.info[:subtitle].include? 1 and
|
1126
|
+
media.info[:subtitle][1][:default] and
|
1127
|
+
not media.info[:subtitle][1][:forced]
|
1128
|
+
Console.info 'Forcing subtitle with mkvpropedit...'
|
1129
|
+
|
1130
|
+
begin
|
1131
|
+
IO.popen([
|
1132
|
+
MKVpropedit.command_name,
|
1133
|
+
'--edit', 'track:s1',
|
1134
|
+
'--set', 'flag-forced=1',
|
1135
|
+
output,
|
1136
|
+
], :err=>[:child, :out]) do |io|
|
1137
|
+
io.each do |line|
|
1138
|
+
Console.debug line
|
1139
|
+
end
|
1140
|
+
end
|
1141
|
+
rescue SystemCallError => e
|
1142
|
+
raise "forcing subtitle failed: #{e}"
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
fail "forcing subtitle: #{output}" if $CHILD_STATUS.exitstatus == 2
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
return if renamed_audio.empty?
|
1149
|
+
|
1150
|
+
media.info[:audio].each do |track, info|
|
1151
|
+
original_name = renamed_audio[info[:name]]
|
1152
|
+
|
1153
|
+
unless original_name.nil?
|
1154
|
+
if media.info[:mkv]
|
1155
|
+
Console.info 'Renaming audio with mkvpropedit...'
|
1156
|
+
|
1157
|
+
begin
|
1158
|
+
IO.popen([
|
1159
|
+
MKVpropedit.command_name,
|
1160
|
+
'--edit', "track:a#{track}",
|
1161
|
+
'--set', "name=#{original_name}",
|
1162
|
+
output,
|
1163
|
+
], :err=>[:child, :out]) do |io|
|
1164
|
+
io.each do |line|
|
1165
|
+
Console.debug line
|
1166
|
+
end
|
1167
|
+
end
|
1168
|
+
rescue SystemCallError => e
|
1169
|
+
raise "renaming audio failed: #{e}"
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
fail "renaming audio: #{output}" if $CHILD_STATUS.exitstatus == 2
|
1173
|
+
elsif media.info[:mp4]
|
1174
|
+
Console.info 'Renaming audio with mp4track...'
|
1175
|
+
|
1176
|
+
['hdlrname', 'udtaname'].each do |property|
|
1177
|
+
begin
|
1178
|
+
IO.popen([
|
1179
|
+
MP4track.command_name,
|
1180
|
+
'--track-index', track.to_s,
|
1181
|
+
"--#{property}", original_name,
|
1182
|
+
output,
|
1183
|
+
], :err=>[:child, :out]) do |io|
|
1184
|
+
io.each do |line|
|
1185
|
+
Console.debug line
|
1186
|
+
end
|
1187
|
+
end
|
1188
|
+
rescue SystemCallError => e
|
1189
|
+
raise "renaming audio failed: #{e}"
|
1190
|
+
end
|
1191
|
+
|
1192
|
+
fail "renaming audio: #{output}" unless $CHILD_STATUS.exitstatus == 0
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
end
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
def terminate
|
1200
|
+
FileUtils.remove_entry @temporary unless @temporary.nil?
|
1201
|
+
end
|
1202
|
+
end
|
1203
|
+
end
|
1204
|
+
|
1205
|
+
VideoTranscoding::Command.new.run
|