video_transcoding 0.1.0
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 +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
|