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.
@@ -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