video_transcoding 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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