other_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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3a57410a06260109a616e8c52a66912c1232b94e1c6c9fa3b68193dc1928a806
4
+ data.tar.gz: 2a3d7e2161ed9e5c057d706dca00505f3579840745e542af1fe65691144f88e4
5
+ SHA512:
6
+ metadata.gz: 01c79a2c59e29c90892ad9a9ced373852c3e81c86994d75ccd45d03b48502d0da4e891bc0852afb75954fe15cb0cbdffb2275b531ad34b33642463173db5d13c
7
+ data.tar.gz: 527078a99a29100e1ffd6031f6d08ef44e9759a09157ab67a1831aa83d60c10544736cbbfe921394237877c640dc595aac530f5cf991e8dc29d9371f2152cb7f
@@ -0,0 +1,9 @@
1
+ # Changes to the "[Other Video Transcoding](https://github.com/donmelton/other_video_transcoding)" project
2
+
3
+ This single document contains all of the notes created for each [release](https://github.com/donmelton/other_video_transcoding/releases).
4
+
5
+ ## [0.1.0](https://github.com/donmelton/other_video_transcoding/releases/tag/0.1.0)
6
+
7
+ Thursday, December 26, 2019
8
+
9
+ * Initial project version.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2019 Don Melton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # Other Video Transcoding
2
+
3
+ Other tools to transcode videos.
4
+
5
+ ## About
6
+
7
+ Hi, I'm [Don Melton](http://donmelton.com/). I created these tools to transcode my collection of Blu-ray Discs and DVDs into a smaller, more portable format while remaining high enough quality to be mistaken for the originals.
8
+
9
+ Unlike my older [Video Transcoding](https://github.com/donmelton/video_transcoding) project, the `other-transcode` tool in this package automatically selects a platform-specific hardware video encoder rather than relying on a slower software encoder.
10
+
11
+ Using an encoder built into a CPU or video card means that even Blu-ray Disc-sized media can be transcoded 5 to 10 times faster than its original playback speed, depending on which hardware is available.
12
+
13
+ But even at those speeds, quality is never compromised because the `other-transcode` tool also selects the best ratecontrol system available within those encoders and properly configures that system. This is what sets it apart from other tools using hardware encoders.
14
+
15
+ Because the `other-transcode` tool leverages [FFmpeg](http://ffmpeg.org/), many hardware platforms are supported including:
16
+
17
+ * [Nvidia NVENC](https://en.wikipedia.org/wiki/Nvidia_NVENC)
18
+ * [Intel Quick Sync Video](https://en.wikipedia.org/wiki/Intel_Quick_Sync_Video)
19
+ * [AMD Video Coding Engine](https://en.wikipedia.org/wiki/Video_Coding_Engine)
20
+ * [Apple VideoToolbox](https://developer.apple.com/documentation/videotoolbox)
21
+
22
+ And many features are supported including:
23
+
24
+ * High quality 10-bit [HEVC](https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding) encoding on recent generations of Nvidia and Intel hardware
25
+ * 8-bit HEVC encoding on other hardware platforms
26
+ * Hardware-based video decoding for improved performance
27
+ * Fallback to software video encoding when appropriate hardware is not available
28
+ * Optional automatic and reliable video cropping
29
+ * Adding audio and subtitle tracks by by language or title
30
+ * [Dolby Digital Plus](https://en.wikipedia.org/wiki/Dolby_Digital_Plus) (Enhanced AC-3) audio encoding
31
+ * Burning image-based subtitles into video output to ease player compatibility
32
+
33
+ Also included in this package is `ask-ffmpeg-log` which reports temporal information from FFmpeg-generated `.log` files containing encoding statistics.
34
+
35
+ Additional documentation for this project is available in the [wiki](https://github.com/donmelton/other_video_transcoding/wiki).
36
+
37
+ ## Installation
38
+
39
+ These tools work on Windows, Linux and macOS. They're packaged as a Gem and require Ruby. See "[Installing Ruby](https://www.ruby-lang.org/en/documentation/installation/)" if you don't have it on your platform.
40
+
41
+ Use this command to install the package:
42
+
43
+ gem install other_video_transcoding
44
+
45
+ And this command to update it:
46
+
47
+ gem update other_video_transcoding
48
+
49
+ The `other-transcode` tool in this package requires other software to function properly, specifically these command line programs:
50
+
51
+ * `ffprobe`
52
+ * `ffmpeg`
53
+ * `mkvpropedit`
54
+
55
+ Optional crop previewing also requires the `mpv` command line program.
56
+
57
+ See "[Download FFmpeg](https://ffmpeg.org/download.html)," "[MKVToolNix Downloads](https://mkvtoolnix.download/downloads.html)" and "[mpv Installation](https://mpv.io/installation/)" to find versions for your platform.
58
+
59
+ ## Usage
60
+
61
+ Each tool in this package has several command line options. The `other-transcode` tool is the most complex with over 50 of its own. Use `--help` to list the options available for a specific tool, along with brief instructions on their usage:
62
+
63
+ other-transcode --help
64
+
65
+ More options for the `other-transcode` tool are available with:
66
+
67
+ other-transcode --help more
68
+
69
+ And the full set of options is available with:
70
+
71
+ other-transcode --help full
72
+
73
+ The `other-transcode` tool automatically determines target video bitrate, main audio track configuration, etc. without any command line options, so using it can be as simple as this on Windows:
74
+
75
+ other-transcode C:\Rips\Movie.mkv
76
+
77
+ Or this on Linux and macOS:
78
+
79
+ other-transcode /Rips/Movie.mkv
80
+
81
+ On completion that command creates two files in the current working directory:
82
+
83
+ Movie.mkv
84
+ Movie.mkv.log
85
+
86
+ The `.log` file can be used as input to the `ask-ffmpeg-log` tool.
87
+
88
+ Use the `--hevc` option to create HEVC video:
89
+
90
+ other-transcode --hevc C:\Rips\Movie.mkv
91
+
92
+ High quality 10-bit HEVC is automatically selected when using the Nvidia and Intel encoders.
93
+
94
+ Use the `--eac3` option to create Dolby Digital Plus audio:
95
+
96
+ other-transcode --eac3 C:\Rips\Movie.mkv
97
+
98
+ ## Feedback
99
+
100
+ Please report bugs or ask questions by [creating a new issue](https://github.com/donmelton/other_video_transcoding/issues) on GitHub. I always try to respond quickly but sometimes it may take as long as 24 hours.
101
+
102
+ ## Acknowledgements
103
+
104
+ This project would not be possible without my collaborators on the [Video Transcoding Slack](https://videotranscoding.slack.com/) who spend countless hours reviewing, testing, documenting and supporting this software.
105
+
106
+ ## License
107
+
108
+ Other Video Transcoding is copyright [Don Melton](http://donmelton.com/) and available under a [MIT license](https://github.com/donmelton/other_video_transcoding/blob/master/LICENSE).
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # ask-ffmpeg-log
4
+ #
5
+ # Copyright (c) 2019 Don Melton
6
+ #
7
+
8
+ require 'abbrev'
9
+ require 'optparse'
10
+
11
+ module Transcoding
12
+
13
+ class UsageError < RuntimeError
14
+ end
15
+
16
+ class Command
17
+ def about
18
+ <<HERE
19
+ ask-ffmpeg-log 0.1.0
20
+ Copyright (c) 2019 Don Melton
21
+ HERE
22
+ end
23
+
24
+ def usage
25
+ <<HERE
26
+ Report temporal information from ffmpeg-generated `.log` files
27
+ containing encoding statistics.
28
+
29
+ Usage: #{$PROGRAM_NAME} [OPTION]... [FILE|DIRECTORY]...
30
+
31
+ Options:
32
+ --time sort results by time instead of speed
33
+ --reverse reverse direction of sort
34
+ --tabular use tab character as field delimiter and suppress labels
35
+ -h, --help display this help and exit
36
+ --version output version information and exit
37
+ HERE
38
+ end
39
+
40
+ def initialize
41
+ @by_time = false
42
+ @reverse = false
43
+ @tabular = false
44
+ @logs = []
45
+ @paths = []
46
+ end
47
+
48
+ def run
49
+ begin
50
+ OptionParser.new do |opts|
51
+ define_options opts
52
+
53
+ opts.on '-h', '--help' do
54
+ puts usage
55
+ exit
56
+ end
57
+
58
+ opts.on '--version' do
59
+ puts about
60
+ exit
61
+ end
62
+ end.parse!
63
+ rescue OptionParser::ParseError => e
64
+ raise UsageError, e
65
+ end
66
+
67
+ fail UsageError, 'missing argument' if ARGV.empty?
68
+ ARGV.each { |arg| process_input arg }
69
+ complete
70
+ exit
71
+ rescue UsageError => e
72
+ Kernel.warn "#{$PROGRAM_NAME}: #{e}\nTry `#{$PROGRAM_NAME} --help more` for more information."
73
+ exit false
74
+ rescue StandardError => e
75
+ Kernel.warn "#{$PROGRAM_NAME}: #{e}"
76
+ exit(-1)
77
+ rescue SignalException
78
+ puts
79
+ exit(-1)
80
+ end
81
+
82
+ def define_options(opts)
83
+ opts.on('--time') { @by_time = true }
84
+ opts.on('--reverse') { @reverse = true }
85
+ opts.on('--tabular') { @tabular = true }
86
+ end
87
+
88
+ def process_input(path)
89
+ input = File.absolute_path(path)
90
+
91
+ if File.directory? input
92
+ logs = Dir[input + File::SEPARATOR + '*.log']
93
+ fail "does not contain `.log` files: #{input}" if logs.empty?
94
+ @logs += logs
95
+ @paths << input
96
+ else
97
+ fail "not a `.log` file: #{input}" unless File.extname(input) == '.log'
98
+ @logs << File.absolute_path(input)
99
+ @paths << File.dirname(input)
100
+ end
101
+ end
102
+
103
+ def complete
104
+ @logs.uniq!
105
+ @paths.uniq!
106
+
107
+ if @paths.size > 1
108
+ prefix = File.dirname(@paths.abbrev.keys.min_by { |key| key.size }) + File::SEPARATOR
109
+ else
110
+ prefix = ''
111
+ end
112
+
113
+ if @tabular
114
+ delimiter = "\t"
115
+ fps_label = ''
116
+ else
117
+ delimiter = ' '
118
+ fps_label = ' fps'
119
+ end
120
+
121
+ report = []
122
+
123
+ @logs.each do |log|
124
+ video = File.basename(log, '.log')
125
+ video += " (#{File.dirname(log).sub(prefix, '')})" unless prefix.empty?
126
+
127
+ begin
128
+ content = File.read(log)
129
+ rescue SystemCallError => e
130
+ raise "reading `.log` file failed: #{e}"
131
+ end
132
+
133
+ unless content.match(/^.*\R/).to_s.chomp =~ /^ffmpeg/
134
+ fail "not a ffmpeg-generated `.log` file: #{log}"
135
+ end
136
+
137
+ stats = content.match(/^frame=.*[.0-9]+x */m).to_s.lines.last.to_s.rstrip
138
+
139
+ if stats =~ /frame=([0-9]+) fps=( *[.0-9]+)/
140
+ frames = $1
141
+ fps = $2
142
+ seconds = frames.to_f / fps.lstrip.to_f
143
+ time = sprintf("%02d:%02d:%02d", seconds / (60 * 60), (seconds / 60) % 60, seconds % 60)
144
+ fps.lstrip! if @tabular
145
+ else
146
+ fps = '0.0'
147
+ time = '00:00:00'
148
+ end
149
+
150
+ time += delimiter
151
+ line = ''
152
+ line += time if @by_time
153
+ line += fps + fps_label + delimiter
154
+ line += time unless @by_time
155
+ report << line + video
156
+ end
157
+
158
+ if @by_time
159
+ report.sort!
160
+ else
161
+ report.sort! do |a, b|
162
+ number_a = a.lstrip.match(/^[.0-9]+/).to_s.to_f
163
+ number_b = b.lstrip.match(/^[.0-9]+/).to_s.to_f
164
+
165
+ if number_a < number_b
166
+ -1
167
+ elsif number_a > number_b
168
+ 1
169
+ else
170
+ a <=> b
171
+ end
172
+ end
173
+ end
174
+
175
+ report.reverse! if (!@reverse and !@by_time) or (@reverse and @by_time)
176
+ puts report
177
+ end
178
+ end
179
+ end
180
+
181
+ Transcoding::Command.new.run
@@ -0,0 +1,1862 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # other-transcode
4
+ #
5
+ # Copyright (c) 2019 Don Melton
6
+ #
7
+
8
+ require 'English'
9
+ require 'fileutils'
10
+ require 'json'
11
+ require 'optparse'
12
+
13
+ module Transcoding
14
+
15
+ class UsageError < RuntimeError
16
+ end
17
+
18
+ class Command
19
+ def about
20
+ <<HERE
21
+ other-transcode 0.1.0
22
+ Copyright (c) 2019 Don Melton
23
+ HERE
24
+ end
25
+
26
+ def usage1
27
+ <<HERE
28
+ Transcode Blu-ray Disc or DVD rip into a smaller, more portable format
29
+ while remaining high enough quality to be mistaken for the original.
30
+
31
+ Usage: #{$PROGRAM_NAME} [OPTION]... [FILE]...
32
+
33
+ Creates Matroska `.mkv` format file in current working directory.
34
+
35
+ Automatically selects a platform-specific hardware video encoder.
36
+
37
+ HERE
38
+ end
39
+
40
+ def usage2
41
+ <<HERE
42
+ Input options:
43
+ --position TIME, --duration TIME
44
+ start transcoding at position and/or limit to duration
45
+ in seconds[.milliseconds] or [HH:]MM:SS[.m...] format
46
+
47
+ HERE
48
+ end
49
+
50
+ def usage3
51
+ <<HERE
52
+ Output options:
53
+ --debug increase diagnostic information
54
+ --preview-crop show commands to preview detected video crop and exit
55
+ HERE
56
+ end
57
+
58
+ def usage4
59
+ <<HERE
60
+ --print-crop print only detected video crop geometry and exit
61
+ --mp4 output MP4 instead of Matroska `.mkv` format
62
+ --name STRING set output filename, excluding format extension
63
+ (default: based on input filename)
64
+ --copy-track-names
65
+ copy all input audio track names to output
66
+ HERE
67
+ end
68
+
69
+ def usage5
70
+ <<HERE
71
+ --max-muxing-queue-size SIZE
72
+ set maximum number of packets to buffer when muxing
73
+ HERE
74
+ end
75
+
76
+ def usage6
77
+ <<HERE
78
+ -n, --dry-run don't transcode, just show `ffmpeg` command and exit
79
+
80
+ Video options:
81
+ --hevc use HEVC version of platform-specific video encoder
82
+ HERE
83
+ end
84
+
85
+ def usage7
86
+ <<HERE
87
+ --vt use Apple Video Toolbox encoder
88
+ --nvenc use Nvidia video encoder
89
+ --qsv use Intel Quick Sync video encoder
90
+ --amf use AMD video encoder
91
+ --vaapi use Video Acceleration API encoder
92
+ --x264 use x264 software video encoder
93
+ --x265 use x265 " " "
94
+ --10-bit, --no-10-bit
95
+ use 10-bit pixel format (default: not used for H.264,
96
+ used for HEVC with Nvidia, Intel and x265 encoders)
97
+ --preset NAME|none
98
+ apply video encoder preset or disable default settings
99
+ --decode vc1|all|none
100
+ set scope of automatic hardware decoder acceleration
101
+ (default: vc1 for VC-1 format only)
102
+ --cuvid use Nvidia video decoder
103
+ for H.264, VC-1, MPEG-2 and other formats
104
+ (ignores scope set by `--decode`)
105
+ HERE
106
+ end
107
+
108
+ def usage8
109
+ <<HERE
110
+ --target [2160p=|1080p=|720p=|480p=]BITRATE
111
+ set video bitrate target (default: based on input)
112
+ or target for specific input resolution
113
+ --crop WIDTH:HEIGHT:X:Y|TOP:BOTTOM:LEFT:RIGHT|auto
114
+ set video crop geometry (default: none)
115
+ or automatically detect it
116
+ --720p fit video within 1280x720 pixel bounds
117
+ HERE
118
+ end
119
+
120
+ def usage9
121
+ <<HERE
122
+ --1080p " " " 1920x1080 " "
123
+ --deinterlace reduce interlace artifacts without changing frame rate
124
+ (applied automatically for some inputs)
125
+ --rate FPS force constant video frame rate
126
+ (`24000/1001` applied automatically for some inputs)
127
+ --detelecine drop duplicate frames to restore original frame rate
128
+ (disables any deinterlacing and forced frame rate)
129
+ --no-filters disable any automatic adjustments via filters
130
+ HERE
131
+ end
132
+
133
+ def usage10
134
+ <<HERE
135
+
136
+ Apple Video Toolbox encoder options:
137
+ --vt-allow-sw allow software encoding
138
+
139
+ Nvidia video encoder options:
140
+ --nvenc-spatial-aq, --no-nvenc-spatial-aq
141
+ enable or disable spatial AQ (default: enabled)
142
+ --nvenc-temporal-aq, --no-nvenc-temporal-aq
143
+ enable or disable temporal AQ
144
+ (default: enabled for H.264, disabled for HEVC)
145
+ --nvenc-lookahead FRAMES
146
+ set number of frames to look ahead for ratecontrol
147
+ --nvenc-refs FRAMES
148
+ set number of reference frames
149
+ --nvenc-bframes FRAMES
150
+ set maximum number of B-frames
151
+
152
+ Intel Quick Sync video encoder options:
153
+ --qsv-refs FRAMES
154
+ set number of reference frames
155
+ --qsv-bframes FRAMES
156
+ set maximum number of B-frames
157
+
158
+ AMD video encoder options:
159
+ --amf-quality balanced|speed|quality
160
+ set quality preference
161
+ --amf-vbaq enable variance based AQ
162
+ --amf-pre-analysis
163
+ enable ratecontrol pre-analysis
164
+ --amf-refs FRAMES
165
+ set maximum number of reference frames
166
+ --amf-bframes FRAMES
167
+ set maximum number of B-frames
168
+
169
+ Video Acceleration API encoder options:
170
+ --vaapi-compression LEVEL
171
+ set numeric level of compression
172
+
173
+ x264 software video encoder options:
174
+ --x264-avbr use average variable bitrate (AVBR) ratecontrol
175
+ --x264-quick increase encoding speed by 70-80%
176
+ with no easily perceptible loss in video quality
177
+ (avoids quality problems with some encoder presets)
178
+ HERE
179
+ end
180
+
181
+ def usage11
182
+ <<HERE
183
+
184
+ Audio options:
185
+ --main-audio TRACK[=WIDTH]
186
+ select main audio track by number (default: 1)
187
+ with optional width (default: surround)
188
+ --add-audio TRACK|LANGUAGE|STRING[=WIDTH]
189
+ add single audio track by number
190
+ including main audio track
191
+ or audio tracks by language code
192
+ excluding main audio track
193
+ (in ISO 639-2 format, e.g.: `eng`)
194
+ or audio tracks with titles containing string
195
+ excluding main audio track
196
+ (comparison is case-insensitve)
197
+ with optional width (default: stereo)
198
+ --surround-bitrate BITRATE
199
+ set surround audio bitrate (default: 640)
200
+ --stereo-bitrate BITRATE
201
+ set stereo audio bitrate (default: 256)
202
+ --eac3 use Enhanced AC-3 format for surround audio
203
+
204
+ Subtitle options:
205
+ --add-subtitle TRACK[=forced]|auto|LANGUAGE|STRING
206
+ add single subtitle track by number
207
+ optionally setting forced disposition
208
+ or enable automatic addition of forced subtitle
209
+ or add subtitle tracks by language code
210
+ (in ISO 639-2 format, e.g.: `eng`)
211
+ or subtitle tracks with titles containing string
212
+ (comparison is case-insensitve)
213
+ (all variations exclude any burned track)
214
+ --burn-subtitle TRACK|auto
215
+ burn subtitle track by number into video
216
+ or enable automatic burning of forced subtitle
217
+ (only image-based subtitles are burned)
218
+
219
+ Other options:
220
+ -h, --help [more|full]
221
+ display help and exit
222
+ optionally including more or full information
223
+ --version output version information and exit
224
+
225
+ Requires `ffprobe`, `ffmpeg` and `mkvpropedit`.
226
+ HERE
227
+ end
228
+
229
+ def initialize
230
+ @position = nil
231
+ @duration = nil
232
+ @debug = false
233
+ @detect = false
234
+ @preview = false
235
+ @format = :mkv
236
+ @name = nil
237
+ @copy_track_names = false
238
+ @max_muxing_queue_size = nil
239
+ @dry_run = false
240
+ @hevc = false
241
+ @encoder = nil
242
+ @ten_bit = nil
243
+ @preset = nil
244
+ @decode_scope = :vc1
245
+ @decoder_type = nil
246
+ @target_2160p = nil
247
+ @target_1080p = nil
248
+ @target_720p = nil
249
+ @target_480p = nil
250
+ @target = nil
251
+ @crop = nil
252
+ @max_width = 3840
253
+ @max_height = 2160
254
+ @deinterlace = false
255
+ @rate = nil
256
+ @detelecine = false
257
+ @enable_filters = true
258
+ @vt_allow_sw = false
259
+ @nvenc_spatial_aq = nil
260
+ @nvenc_temporal_aq = nil
261
+ @nvenc_lookahead = nil
262
+ @nvenc_refs = nil
263
+ @nvenc_bframes = nil
264
+ @qsv_refs = nil
265
+ @qsv_bframes = nil
266
+ @amf_quality = nil
267
+ @amf_vbaq = false
268
+ @amf_pre_analysis = false
269
+ @amf_refs = nil
270
+ @amf_bframes = nil
271
+ @vaapi_compression = nil
272
+ @x264_avbr = false
273
+ @x264_quick = false
274
+ @audio_selections = [{
275
+ :track => 1,
276
+ :language => nil,
277
+ :title => nil,
278
+ :width => :surround
279
+ }]
280
+ @surround_bitrate = 640
281
+ @stereo_bitrate = 256
282
+ @surround_encoder = 'ac3'
283
+ @stereo_encoder = nil
284
+ @subtitle_selections = []
285
+ @auto_add_subtitle = false
286
+ @burn_subtitle_track = 0
287
+ end
288
+
289
+ def run
290
+ begin
291
+ OptionParser.new do |opts|
292
+ define_options opts
293
+
294
+ opts.on '-h', '--help [ARG]' do |arg|
295
+ case arg
296
+ when 'full'
297
+ puts usage1 + usage2 + usage3 + usage4 + usage5 + usage6 +
298
+ usage7 + usage8 + usage9 + usage10 + usage11
299
+ when 'more'
300
+ puts usage1 + usage2 + usage3 + usage4 + usage6 + usage7 +
301
+ usage8 + usage9 + usage11
302
+ else
303
+ puts usage1 + usage3 + usage6 + usage8 + usage11
304
+ end
305
+
306
+ exit
307
+ end
308
+
309
+ opts.on '--version' do
310
+ puts about
311
+ exit
312
+ end
313
+ end.parse!
314
+ rescue OptionParser::ParseError => e
315
+ raise UsageError, e
316
+ end
317
+
318
+ fail UsageError, 'missing argument' if ARGV.empty?
319
+ configure ARGV.first
320
+ ARGV.each { |arg| process_input arg }
321
+ exit
322
+ rescue UsageError => e
323
+ Kernel.warn "#{$PROGRAM_NAME}: #{e}\nTry `#{$PROGRAM_NAME} --help more` for more information."
324
+ exit false
325
+ rescue StandardError => e
326
+ Kernel.warn "#{$PROGRAM_NAME}: #{e}"
327
+ exit(-1)
328
+ rescue SignalException
329
+ puts
330
+ exit(-1)
331
+ end
332
+
333
+ def define_options(opts)
334
+ opts.on '--position ARG' do |arg|
335
+ @position = resolve_time(arg)
336
+ end
337
+
338
+ opts.on '--duration ARG' do |arg|
339
+ @duration = resolve_time(arg)
340
+ end
341
+
342
+ opts.on '--debug' do
343
+ @debug = true
344
+ end
345
+
346
+ opts.on '--preview-crop' do
347
+ @detect = true
348
+ @preview = true
349
+ end
350
+
351
+ opts.on '--print-crop' do
352
+ @detect = true
353
+ @preview = false
354
+ end
355
+
356
+ opts.on '--mp4' do
357
+ @format = :mp4
358
+ end
359
+
360
+ opts.on '--name ARG' do |arg|
361
+ @name = arg
362
+ end
363
+
364
+ opts.on '--copy-track-names' do
365
+ @copy_track_names = true
366
+ end
367
+
368
+ opts.on '--max-muxing-queue-size ARG', Integer do |arg|
369
+ @max_muxing_queue_size = [arg, 1].max
370
+ end
371
+
372
+ opts.on '-n', '--dry-run' do
373
+ @dry_run = true
374
+ end
375
+
376
+ opts.on '--hevc' do
377
+ @encoder = 'libx265' if @encoder == 'libx264'
378
+ @hevc = true
379
+ end
380
+
381
+ opts.on '--vt' do
382
+ @encoder = @hevc ? 'hevc_videotoolbox' : 'h264_videotoolbox'
383
+ end
384
+
385
+ opts.on '--nvenc' do
386
+ @encoder = @hevc ? 'hevc_nvenc' : 'h264_nvenc'
387
+ end
388
+
389
+ opts.on '--qsv' do
390
+ @encoder = @hevc ? 'hevc_qsv' : 'h264_qsv'
391
+ end
392
+
393
+ opts.on '--amf' do
394
+ @encoder = @hevc ? 'hevc_amf' : 'h264_amf'
395
+ end
396
+
397
+ opts.on '--vaapi' do
398
+ @encoder = @hevc ? 'hevc_vaapi' : 'h264_vaapi'
399
+ end
400
+
401
+ opts.on '--x264' do
402
+ @encoder = 'libx264'
403
+ @hevc = false
404
+ end
405
+
406
+ opts.on '--x265' do
407
+ @encoder = 'libx265'
408
+ @hevc = true
409
+ end
410
+
411
+ opts.on '--[no-]10-bit' do |arg|
412
+ @ten_bit = arg
413
+ end
414
+
415
+ opts.on '--preset ARG' do |arg|
416
+ @preset = arg
417
+ end
418
+
419
+ opts.on '--decode ARG' do |arg|
420
+ @decode_scope = case arg
421
+ when 'vc1', 'all', 'none'
422
+ arg.to_sym
423
+ else
424
+ fail UsageError, "invalid scope for automatic hardware decoder usage: #{$1}"
425
+ end
426
+ end
427
+
428
+ opts.on '--cuvid' do
429
+ @decoder_type = :cuvid
430
+ end
431
+
432
+ opts.on '--target ARG' do |arg|
433
+ if arg =~ /^([0-9]+p)=([1-9][0-9]*)$/
434
+ bitrate = [$2.to_i, 1].max
435
+
436
+ case $1
437
+ when '2160p'
438
+ @target_2160p = bitrate
439
+ when '1080p'
440
+ @target_1080p = bitrate
441
+ when '720p'
442
+ @target_720p = bitrate
443
+ when '480p'
444
+ @target_480p = bitrate
445
+ else
446
+ fail UsageError, "invalid target video bitrate resolution: #{$1}"
447
+ end
448
+
449
+ @target = nil
450
+ else
451
+ @target = [arg.to_i, 1].max
452
+ end
453
+ end
454
+
455
+ opts.on '--crop ARG' do |arg|
456
+ case arg
457
+ when /^([0-9]+):([0-9]+):([0-9]+):([0-9]+)$/
458
+ @crop = [$1.to_i, $2.to_i, $3.to_i, $4.to_i]
459
+ when 'auto'
460
+ @crop = arg.to_sym
461
+ else
462
+ fail UsageError, "invalid crop geometry: #{arg}"
463
+ end
464
+ end
465
+
466
+ opts.on '--720p' do
467
+ @max_width = 1280
468
+ @max_height = 720
469
+ end
470
+
471
+ opts.on '--1080p' do
472
+ @max_width = 1920
473
+ @max_height = 1080
474
+ end
475
+
476
+ opts.on '--deinterlace' do
477
+ @deinterlace = true
478
+ @detelecine = false
479
+ @enable_filters = false
480
+ end
481
+
482
+ opts.on '--rate ARG' do |arg|
483
+ @rate = case arg
484
+ when /(24000|30000|60000)\/1001/, /(24|25)\/1/
485
+ arg
486
+ when '23.976', 'film'
487
+ '24000/1001'
488
+ when 'pal'
489
+ '25/1'
490
+ when '29.97', 'ntsc'
491
+ '30000/1001'
492
+ when '59.94'
493
+ '60000/1001'
494
+ when /^[0-9]+$/
495
+ [[arg.to_i, 1].max, 1000].min.to_s + '/1'
496
+ else
497
+ fail UsageError, "invalid frame rate: #{arg}"
498
+ end
499
+
500
+ @detelecine = false
501
+ @enable_filters = false
502
+ end
503
+
504
+ opts.on '--detelecine' do
505
+ @detelecine = true
506
+ @deinterlace = false
507
+ @rate = nil
508
+ @enable_filters = false
509
+ end
510
+
511
+ opts.on '--no-filters' do
512
+ @enable_filters = false
513
+ end
514
+
515
+ opts.on '--vt-allow-sw' do
516
+ @encoder = @hevc ? 'hevc_videotoolbox' : 'h264_videotoolbox'
517
+ @vt_allow_sw = true
518
+ end
519
+
520
+ opts.on '--[no-]nvenc-spatial-aq' do |arg|
521
+ @encoder = @hevc ? 'hevc_nvenc' : 'h264_nvenc'
522
+ @nvenc_spatial_aq = arg
523
+ end
524
+
525
+ opts.on '--[no-]nvenc-temporal-aq' do |arg|
526
+ @encoder = @hevc ? 'hevc_nvenc' : 'h264_nvenc'
527
+ @nvenc_temporal_aq = arg
528
+ end
529
+
530
+ opts.on '--nvenc-lookahead ARG', Integer do |arg|
531
+ @encoder = @hevc ? 'hevc_nvenc' : 'h264_nvenc'
532
+ @nvenc_lookahead = [[arg, 0].max, 32].min
533
+ end
534
+
535
+ opts.on '--nvenc-refs ARG', Integer do |arg|
536
+ @encoder = @hevc ? 'hevc_nvenc' : 'h264_nvenc'
537
+ @nvenc_refs = [arg, 0].max
538
+ end
539
+
540
+ opts.on '--nvenc-bframes ARG', Integer do |arg|
541
+ @encoder = @hevc ? 'hevc_nvenc' : 'h264_nvenc'
542
+ @nvenc_bframes = [[arg, 0].max, 4].min
543
+ end
544
+
545
+ opts.on '--qsv-refs ARG', Integer do |arg|
546
+ @encoder = @hevc ? 'hevc_qsv' : 'h264_qsv'
547
+ @qsv_refs = [arg, 0].max
548
+ end
549
+
550
+ opts.on '--qsv-bframes ARG', Integer do |arg|
551
+ @encoder = @hevc ? 'hevc_qsv' : 'h264_qsv'
552
+ @qsv_bframes = [arg, -1].max
553
+ end
554
+
555
+ opts.on '--amf-quality ARG' do |arg|
556
+ @encoder = @hevc ? 'hevc_amf' : 'h264_amf'
557
+
558
+ @amf_quality = case arg
559
+ when 'balanced', 'speed', 'quality'
560
+ arg
561
+ else
562
+ fail UsageError, "invalid quality argument: #{arg}"
563
+ end
564
+ end
565
+
566
+ opts.on '--amf-vbaq' do
567
+ @encoder = @hevc ? 'hevc_amf' : 'h264_amf'
568
+ @amf_vbaq = true
569
+ end
570
+
571
+ opts.on '--amf-pre_analysis' do
572
+ @encoder = @hevc ? 'hevc_amf' : 'h264_amf'
573
+ @amf_pre_analysis = true
574
+ end
575
+
576
+ opts.on '--amf-refs ARG', Integer do |arg|
577
+ @encoder = @hevc ? 'hevc_amf' : 'h264_amf'
578
+ @amf_refs = [arg, 0].max
579
+ end
580
+
581
+ opts.on '--amf-bframes ARG', Integer do |arg|
582
+ @encoder = @hevc ? 'hevc_amf' : 'h264_amf'
583
+ @amf_bframes = [arg, 1].max
584
+ end
585
+
586
+ opts.on '--vaapi-compression ARG', Integer do |arg|
587
+ @encoder = @hevc ? 'hevc_vaapi' : 'h264_vaapi'
588
+ @vaapi_compression = [arg, 0].max
589
+ end
590
+
591
+ opts.on '--x264-avbr' do
592
+ @encoder = 'libx264'
593
+ @hevc = false
594
+ @x264_avbr = true
595
+ end
596
+
597
+ opts.on '--x264-quick' do
598
+ @encoder = 'libx264'
599
+ @hevc = false
600
+ @x264_quick = true
601
+ @preset = nil
602
+ end
603
+
604
+ opts.on '--main-audio ARG' do |arg|
605
+ if arg =~ /^([0-9]+)(?:=(surround|stereo))?$/
606
+ @audio_selections[0][:track] = $1.to_i
607
+ @audio_selections[0][:width] = $2.to_sym unless $2.nil?
608
+ else
609
+ fail UsageError, "invalid main audio argument: #{arg}"
610
+ end
611
+ end
612
+
613
+ opts.on '--add-audio ARG' do |arg|
614
+ if arg =~ /^([^=]+)(?:=(surround|stereo))?$/
615
+ scope = $1
616
+ width = $2
617
+
618
+ selection = {
619
+ :track => nil,
620
+ :language => nil,
621
+ :title => nil,
622
+ :width => :stereo
623
+ }
624
+
625
+ case scope
626
+ when /^[0-9]+$/
627
+ selection[:track] = scope.to_i
628
+ when /^[a-z]{3}$/
629
+ selection[:language] = scope
630
+ else
631
+ selection[:title] = scope
632
+ end
633
+
634
+ selection[:width] = width.to_sym unless width.nil?
635
+ @audio_selections += [selection]
636
+ else
637
+ fail UsageError, "invalid add audio argument: #{arg}"
638
+ end
639
+ end
640
+
641
+ opts.on '--surround-bitrate ARG', Integer do |arg|
642
+ @surround_bitrate = arg
643
+ end
644
+
645
+ opts.on '--stereo-bitrate ARG', Integer do |arg|
646
+ @stereo_bitrate = arg
647
+ end
648
+
649
+ opts.on '--eac3' do
650
+ @surround_encoder = 'eac3'
651
+ end
652
+
653
+ opts.on '--add-subtitle ARG' do |arg|
654
+ if arg =~ /^([0-9]+)(?:=(forced))?$|^(auto)$|^([a-z]{3})$|^(.*)$/
655
+ @subtitle_selections += [{
656
+ :track => $1.to_i,
657
+ :forced => $2.nil? ? false : true,
658
+ :language => $4,
659
+ :title => $5
660
+ }]
661
+
662
+ @auto_add_subtitle = false unless $2.nil?
663
+ @auto_add_subtitle = true unless $3.nil?
664
+ else
665
+ fail UsageError, "invalid add subtitle argument: #{arg}"
666
+ end
667
+ end
668
+
669
+ opts.on '--burn-subtitle ARG' do |arg|
670
+ @burn_subtitle_track = case arg
671
+ when /^[0-9]+$/
672
+ arg.to_i
673
+ when 'auto'
674
+ arg.to_sym
675
+ else
676
+ fail UsageError, "invalid subtitle track: #{arg}"
677
+ end
678
+ end
679
+ end
680
+
681
+ def resolve_time(arg)
682
+ time = 0.0
683
+
684
+ case arg
685
+ when /^([0-9]+(?:\.[0-9]+)?)$/
686
+ time = $1.to_f
687
+ when /^(?:(?:([0-9][0-9]):)?([0-9][0-9]):)?([0-9][0-9](?:\.[0-9]+)?)$/
688
+ time = $3.to_f
689
+ time = ($2.to_i * 60) + time unless $2.nil?
690
+ time = ($1.to_i * 60 * 60) + time unless $1.nil?
691
+ else
692
+ fail UsageError, "invalid time: #{arg}"
693
+ end
694
+
695
+ time
696
+ end
697
+
698
+ def configure(path)
699
+ @audio_selections.uniq!
700
+ @subtitle_selections.uniq!
701
+ @surround_bitrate = [[@surround_bitrate, 256].max, (@surround_encoder == 'ac3' ? 640 : 768)].min
702
+ @stereo_bitrate = [[@stereo_bitrate, 128].max, 256].min
703
+
704
+ [
705
+ ['ffprobe', '-loglevel', 'quiet', '-version'],
706
+ ['ffmpeg', '-loglevel', 'quiet', '-version'],
707
+ ['mkvpropedit', '--version']
708
+ ].each do |command|
709
+ verify_tool_availability command
710
+ end
711
+
712
+ return if @detect
713
+
714
+ encoders = find_encoders
715
+
716
+ if @encoder.nil?
717
+ standard = @hevc ? 'hevc' : 'h264'
718
+ name = "#{standard}_videotoolbox"
719
+
720
+ if encoders =~ /#{name}/
721
+ @encoder = name if try_encoder(name, path)
722
+ else
723
+ ['nvenc', 'qsv', 'amf', 'vaapi'].each do |platform|
724
+ name = standard + '_' + platform
725
+
726
+ if encoders =~ /#{name}/ and try_encoder(name, path)
727
+ @encoder = name
728
+ break
729
+ end
730
+ end
731
+ end
732
+
733
+ @encoder ||= @hevc ? 'libx265' : 'libx264'
734
+ else
735
+ @encoder.sub!(/^h264/, 'hevc') if @hevc
736
+
737
+ unless @dry_run or encoders =~ /#{@encoder}/
738
+ fail "video encoder not available: #{@encoder}"
739
+ end
740
+ end
741
+
742
+ @ten_bit = (@hevc and @encoder =~ /(nvenc|qsv|x265)$/ ? true : false) if @ten_bit.nil?
743
+ @target_2160p ||= (@hevc and @ten_bit) ? 12000 : 16000
744
+ @target_1080p ||= (@hevc and @ten_bit) ? 6000 : 8000
745
+ @target_720p ||= (@hevc and @ten_bit) ? 3000 : 4000
746
+ @target_480p ||= (@hevc and @ten_bit) ? 1500 : 2000
747
+
748
+ if @stereo_encoder.nil?
749
+ if encoders =~ /aac_at/ or encoders =~ /libfdk_aac/
750
+ @stereo_encoder = $MATCH
751
+ else
752
+ @stereo_encoder = 'aac'
753
+ end
754
+ end
755
+ end
756
+
757
+ def verify_tool_availability(command)
758
+ Kernel.warn "Verifying \"#{command[0]}\" availability..."
759
+
760
+ begin
761
+ IO.popen(command, :err=>[:child, :out]) do |io|
762
+ io.each do |line|
763
+ Kernel.warn line if @debug
764
+ end
765
+ end
766
+ rescue SystemCallError => e
767
+ raise "verifying tool availability failed: #{e}"
768
+ end
769
+
770
+ fail "verifying tool availability failed: #{command[0]}" unless $CHILD_STATUS.exitstatus == 0
771
+ end
772
+
773
+ def find_encoders
774
+ Kernel.warn 'Finding encoders...'
775
+ output = ''
776
+
777
+ begin
778
+ IO.popen([
779
+ 'ffmpeg',
780
+ '-loglevel', 'quiet',
781
+ '-encoders'
782
+ ], :err=>[:child, :out]) do |io|
783
+ io.each do |line|
784
+ Kernel.warn line if @debug
785
+ output += line
786
+ end
787
+ end
788
+ rescue SystemCallError => e
789
+ raise "finding encoders failed: #{e}"
790
+ end
791
+
792
+ fail 'finding encoders failed' unless $CHILD_STATUS.exitstatus == 0
793
+
794
+ output
795
+ end
796
+
797
+ def try_encoder(encoder, path)
798
+ Kernel.warn "Trying \"#{encoder}\" video encoder..."
799
+ begin
800
+ IO.popen([
801
+ 'ffmpeg',
802
+ '-loglevel', 'quiet',
803
+ '-nostdin'
804
+ ] + (encoder =~ /vaapi$/ ? ['-vaapi_device', '/dev/dri/renderD128'] : []) + [
805
+ '-i', path,
806
+ '-frames:v', '1'
807
+ ] + (encoder =~ /vaapi$/ ? ['-filter:v', 'format=nv12,hwupload'] : []) + [
808
+ '-c:v', encoder,
809
+ '-b:v', '1000k'
810
+ ] + (encoder =~ /nvenc$/ ? ['-rc:v', 'vbr_hq', '-spatial-aq:v', '1'] : []) +
811
+ (encoder == 'h264_nvenc' ? ['-temporal-aq:v', '1'] : []) +
812
+ (encoder == 'h264_qsv' ? ['-look_ahead:v', '1'] : []) +
813
+ (encoder == 'hevc_qsv' ? ['-load_plugin:v', 'hevc_hw'] : []) +
814
+ (encoder =~ /amf$/ ? ['-rc:v', 'vbr_latency'] : []) + [
815
+ '-an',
816
+ '-sn',
817
+ '-ignore_unknown',
818
+ '-f', 'null',
819
+ '-'
820
+ ], :err=>[:child, :out]) do |io|
821
+ io.each do |line|
822
+ Kernel.warn line if @debug
823
+ end
824
+ end
825
+ rescue SystemCallError => e
826
+ raise "trying \"#{encoder}\" encoder failed: #{e}"
827
+ end
828
+
829
+ $CHILD_STATUS.exitstatus == 0
830
+ end
831
+
832
+ def process_input(path)
833
+ seconds = Time.now.tv_sec
834
+
835
+ unless @detect
836
+ output_path = (@name.nil? ? File.basename(path, '.*') : @name) + '.' + @format.to_s
837
+ fail "output file already exists: #{output_path}" if File.exist? output_path
838
+
839
+ log_path = output_path + '.log'
840
+ fail "log file already exists: #{log_path}" if File.exist? log_path
841
+
842
+ tmp_log_path = "_ffmpeg_#{rand(10000..99999)}_#{$PROCESS_ID}.#{@format.to_s}.log"
843
+ fail "log file already exists: #{tmp_log_path}" if File.exist? tmp_log_path
844
+ end
845
+
846
+ media_info = scan_media(path)
847
+
848
+ video, burn_subtitle = get_video_streams(media_info)
849
+ fail "video track not found: #{path}" if video.nil?
850
+
851
+ max_x = video['width'] / 4
852
+ max_y = video['height'] / 4
853
+
854
+ if @detect or @crop == :auto
855
+ crop = detect_crop(media_info, video)
856
+
857
+ if @detect
858
+ present_crop(crop, path)
859
+ return
860
+ else
861
+ Kernel.warn "crop = #{crop[:width]}:#{crop[:height]}:#{crop[:x]}:#{crop[:y]}"
862
+ end
863
+ elsif @crop.nil?
864
+ crop = nil
865
+ elsif @crop[2] <= max_x and @crop[3] <= max_x and @crop[0] <= max_y and @crop[1] <= max_y
866
+ Kernel.warn 'Interpreting crop geometry as TOP:BOTTOM:LEFT:RIGHT values...'
867
+ crop = {
868
+ :width => video['width'] - (@crop[2] + @crop[3]),
869
+ :height => video['height'] - (@crop[0] + @crop[1]),
870
+ :x => @crop[2],
871
+ :y => @crop[0]
872
+ }
873
+ else
874
+ crop = {
875
+ :width => @crop[0],
876
+ :height => @crop[1],
877
+ :x => @crop[2],
878
+ :y => @crop[3]
879
+ }
880
+ end
881
+
882
+ time_options = get_time_options(media_info, burn_subtitle)
883
+ decode_options, encode_options = get_video_options(media_info, video, burn_subtitle, crop)
884
+
885
+ ffmpeg_command = [
886
+ 'ffmpeg',
887
+ '-loglevel', (@debug ? 'verbose' : 'error'),
888
+ '-stats'
889
+ ] + time_options +
890
+ decode_options + [
891
+ '-i', "#{path}"
892
+ ] + (@max_muxing_queue_size.nil? ? [] : ['-max_muxing_queue_size', @max_muxing_queue_size.to_s]) +
893
+ encode_options +
894
+ get_audio_options(media_info) +
895
+ get_subtitle_options(media_info, burn_subtitle) + [
896
+ '-metadata:g', 'title='
897
+ ] + (@format == :mp4 ? ['-movflags', 'disable_chpl'] : []) + [
898
+ output_path
899
+ ]
900
+
901
+ command_line = escape_command(ffmpeg_command)
902
+ Kernel.warn 'Command line:'
903
+
904
+ if @dry_run
905
+ puts command_line
906
+ return
907
+ end
908
+
909
+ Kernel.warn command_line
910
+ Kernel.warn 'Transcoding...'
911
+ output = ''
912
+
913
+ begin
914
+ IO.popen({
915
+ 'FFREPORT' => "file=#{tmp_log_path}:level=40"
916
+ }, ffmpeg_command, 'rb', :err=>[:child, :out]) do |io|
917
+ Signal.trap 'INT' do
918
+ Process.kill 'INT', io.pid
919
+ end
920
+
921
+ io.each_char do |char|
922
+ output += char
923
+ STDERR.print char
924
+ end
925
+ end
926
+ rescue SystemCallError => e
927
+ raise "transcoding failed: #{e}"
928
+ end
929
+
930
+ fail "transcoding failed: #{output_path}" unless $CHILD_STATUS.exitstatus == 0
931
+
932
+ if File.exist? log_path
933
+ Kernel.warn '**********'
934
+ Kernel.warn "log file already exists: #{log_path}"
935
+ Kernel.warn "using temporary filename for assembled log: #{tmp_log_path}"
936
+ Kernel.warn '**********'
937
+ log_path = tmp_log_path
938
+ else
939
+ FileUtils.mv tmp_log_path, log_path
940
+ end
941
+
942
+ assemble_log(log_path, output)
943
+
944
+ if @format == :mp4
945
+ Kernel.warn 'Done.'
946
+ else
947
+ add_track_statistics_tags(output_path)
948
+ end
949
+
950
+ Kernel.warn "\nElapsed time: #{seconds_to_time(Time.now.tv_sec - seconds)}\n\n"
951
+ end
952
+
953
+ def scan_media(path)
954
+ Kernel.warn 'Scanning media...'
955
+ output = ''
956
+
957
+ begin
958
+ IO.popen([
959
+ 'ffprobe',
960
+ '-loglevel', 'quiet',
961
+ '-show_streams',
962
+ '-show_format',
963
+ '-print_format', 'json',
964
+ path
965
+ ], :err=>[:child, :out]) do |io|
966
+ io.each do |line|
967
+ Kernel.warn line if @debug
968
+ output += line
969
+ end
970
+ end
971
+ rescue SystemCallError => e
972
+ raise "scanning media failed: #{e}"
973
+ end
974
+
975
+ fail "scanning media failed: #{path}" unless $CHILD_STATUS.exitstatus == 0
976
+
977
+ begin
978
+ media_info = JSON.parse(output)
979
+ rescue JSON::JSONError
980
+ fail "media information not found: #{path}"
981
+ end
982
+
983
+ Kernel.warn media_info.inspect if @debug
984
+ media_info
985
+ end
986
+
987
+ def detect_crop(media_info, video)
988
+ Kernel.warn 'Detecting crop...'
989
+ duration = media_info['format']['duration'].to_f
990
+ fail "media duration too short: #{duration}" if duration < 2.0
991
+ steps = 10
992
+ interval = (duration / (steps + 1)).to_i
993
+ target_interval = 5 * 60
994
+
995
+ if interval == 0
996
+ steps = 1
997
+ interval = 1
998
+ elsif interval > target_interval
999
+ steps = ((duration / target_interval) - 1).to_i
1000
+ interval = (duration / (steps + 1)).to_i
1001
+ end
1002
+
1003
+ Kernel.warn "duration = #{duration} / steps = #{steps} / interval = #{interval}" if @debug
1004
+ width = video['width'].to_i
1005
+ height = video['height'].to_i
1006
+
1007
+ no_crop = {
1008
+ :width => width,
1009
+ :height => height,
1010
+ :x => 0,
1011
+ :y => 0
1012
+ }
1013
+
1014
+ all_crop = {
1015
+ :width => 0,
1016
+ :height => 0,
1017
+ :x => width,
1018
+ :y => height
1019
+ }
1020
+
1021
+ crop = all_crop.dup
1022
+ last_crop = crop.dup
1023
+ ignore_count = 0
1024
+ last_seconds = Time.now.tv_sec
1025
+ path = media_info['format']['filename']
1026
+
1027
+ (1..steps).each do |step|
1028
+ s_crop = all_crop.dup
1029
+
1030
+ begin
1031
+ position = (interval * step)
1032
+
1033
+ if @debug
1034
+ Kernel.warn "crop = #{crop}"
1035
+ Kernel.warn "step = #{step} / position = #{position}"
1036
+ end
1037
+
1038
+ IO.popen([
1039
+ 'ffmpeg',
1040
+ '-hide_banner',
1041
+ '-nostdin',
1042
+ '-noaccurate_seek',
1043
+ '-ss', position.to_s,
1044
+ '-i', path,
1045
+ '-frames:v', '15',
1046
+ '-filter:v', 'cropdetect=24:2',
1047
+ '-an',
1048
+ '-sn',
1049
+ '-ignore_unknown',
1050
+ '-f', 'null',
1051
+ '-'
1052
+ ], :err=>[:child, :out]) do |io|
1053
+ io.each do |line|
1054
+ seconds = Time.now.tv_sec
1055
+
1056
+ if seconds - last_seconds >= 3
1057
+ Kernel.warn '...'
1058
+ last_seconds = seconds
1059
+ end
1060
+
1061
+ if line =~ / crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)$/
1062
+ d_width, d_height, d_x, d_y = $1.to_i, $2.to_i, $3.to_i, $4.to_i
1063
+ s_crop[:width] = d_width if s_crop[:width] < d_width
1064
+ s_crop[:height] = d_height if s_crop[:height] < d_height
1065
+ s_crop[:x] = d_x if s_crop[:x] > d_x
1066
+ s_crop[:y] = d_y if s_crop[:y] > d_y
1067
+ Kernel.warn line if @debug
1068
+ end
1069
+ end
1070
+ end
1071
+ rescue SystemCallError => e
1072
+ raise "crop detection failed: #{e}"
1073
+ end
1074
+
1075
+ fail 'crop detection failed' unless $CHILD_STATUS.exitstatus == 0
1076
+
1077
+ if s_crop == no_crop and last_crop != no_crop
1078
+ ignore_count += 1
1079
+ Kernel.warn "ignore crop = #{s_crop}" if @debug
1080
+ else
1081
+ crop[:width] = s_crop[:width] if crop[:width] < s_crop[:width]
1082
+ crop[:height] = s_crop[:height] if crop[:height] < s_crop[:height]
1083
+ crop[:x] = s_crop[:x] if crop[:x] > s_crop[:x]
1084
+ crop[:y] = s_crop[:y] if crop[:y] > s_crop[:y]
1085
+ end
1086
+
1087
+ last_crop = s_crop.dup
1088
+ end
1089
+
1090
+ Kernel.warn "ignore count = #{ignore_count}" if @debug
1091
+
1092
+ if crop == all_crop or
1093
+ ignore_count > 2 or (
1094
+ ignore_count > 0 and (((crop[:width] + 2) == width and crop[:height] == height))
1095
+ )
1096
+ crop = no_crop
1097
+ end
1098
+
1099
+ crop
1100
+ end
1101
+
1102
+ def present_crop(crop, path)
1103
+ crop_string = "#{crop[:width]}:#{crop[:height]}:#{crop[:x]}:#{crop[:y]}"
1104
+
1105
+ if @preview
1106
+ drawbox_string = "#{crop[:x]}:#{crop[:y]}:#{crop[:width]}:#{crop[:height]}"
1107
+ puts
1108
+ puts escape_command([
1109
+ 'mpv', '--no-audio', '--vf', "lavfi=[drawbox=#{drawbox_string}:invert:1]", path
1110
+ ])
1111
+ puts escape_command([
1112
+ 'mpv', '--no-audio', '--vf', "crop=#{crop_string}", path
1113
+ ])
1114
+ puts
1115
+ puts escape_command([
1116
+ File.basename($PROGRAM_NAME), '--crop', crop_string, path
1117
+ ])
1118
+ puts
1119
+ else
1120
+ puts crop_string
1121
+ end
1122
+ end
1123
+
1124
+ def escape_command(command)
1125
+ command_line = ''
1126
+ command.each {|item| command_line += "#{escape_string(item)} " }
1127
+ command_line.sub!(/ $/, '')
1128
+ command_line
1129
+ end
1130
+
1131
+ def escape_string(str)
1132
+ # See: https://github.com/larskanis/shellwords
1133
+ return '""' if str.empty?
1134
+
1135
+ str = str.dup
1136
+
1137
+ if RUBY_PLATFORM =~ /mingw/
1138
+ str.gsub!(/((?:\\)*)"/) { "\\" * ($1.length * 2) + "\\\"" }
1139
+
1140
+ if str =~ /\s/
1141
+ str.gsub!(/(\\+)\z/) { "\\" * ($1.length * 2 ) }
1142
+ str = "\"#{str}\""
1143
+ end
1144
+ else
1145
+ str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")
1146
+ str.gsub!(/\n/, "'\n'")
1147
+ end
1148
+
1149
+ str
1150
+ end
1151
+
1152
+ def get_video_streams(media_info)
1153
+ video = nil
1154
+ subtitle_track = 0
1155
+ burn_subtitle = nil
1156
+
1157
+ media_info['streams'].each do |stream|
1158
+ case stream['codec_type']
1159
+ when 'video'
1160
+ video = stream if video.nil?
1161
+ when 'subtitle'
1162
+ subtitle_track += 1
1163
+
1164
+ if stream['codec_name'] == 'hdmv_pgs_subtitle' or stream['codec_name'] == 'dvd_subtitle'
1165
+ if @burn_subtitle_track == :auto
1166
+ burn_subtitle = stream if stream['disposition']['forced'] == 1
1167
+ else
1168
+ burn_subtitle = stream if @burn_subtitle_track == subtitle_track
1169
+ end
1170
+ end
1171
+ end
1172
+ end
1173
+
1174
+ return video, burn_subtitle
1175
+ end
1176
+
1177
+ def get_time_options(media_info, burn_subtitle)
1178
+ duration = media_info['format']['duration'].to_f
1179
+ fail "media duration too short: #{duration}" if duration < 2.0
1180
+
1181
+ if @position.nil?
1182
+ position = 0.0
1183
+ else
1184
+ position = [duration - 1.0, @position].min
1185
+ duration -= position
1186
+ end
1187
+
1188
+ duration = [duration, [@duration, 0.1].max].min unless @duration.nil?
1189
+ options = []
1190
+
1191
+ unless burn_subtitle.nil? and @position.nil?
1192
+ options += ['-ss', position.to_s.sub(/\.0$/, '')]
1193
+ end
1194
+
1195
+ unless burn_subtitle.nil? and @duration.nil?
1196
+ options += ['-t', duration.to_s.sub(/\.0$/, '')]
1197
+ end
1198
+
1199
+ time = seconds_to_time(duration.to_i)
1200
+ milliseconds = duration.to_s.sub(/^[0-9]+(\.[0-9]+)$/, '\1')
1201
+ time += milliseconds unless milliseconds == '.0'
1202
+ Kernel.warn "duration = #{time}"
1203
+ options
1204
+ end
1205
+
1206
+ def seconds_to_time(seconds)
1207
+ sprintf("%02d:%02d:%02d", seconds / (60 * 60), (seconds / 60) % 60, seconds % 60)
1208
+ end
1209
+
1210
+ def get_video_options(media_info, video, burn_subtitle, crop)
1211
+ if @decoder_type == :cuvid
1212
+ cuvid_decoder = case video['codec_name']
1213
+ when 'mpeg1video'
1214
+ 'mpeg1_cuvid'
1215
+ when 'mpeg2video'
1216
+ 'mpeg2_cuvid'
1217
+ when 'mjpeg'
1218
+ 'mjpeg_cuvid'
1219
+ when 'mpeg4'
1220
+ 'mpeg4_cuvid'
1221
+ when 'h264'
1222
+ 'h264_cuvid'
1223
+ when 'vc1'
1224
+ 'vc1_cuvid'
1225
+ when 'vp8'
1226
+ 'vp8_cuvid'
1227
+ when 'vp9'
1228
+ 'vp9_cuvid'
1229
+ when 'hevc'
1230
+ 'hevc_cuvid'
1231
+ end
1232
+ else
1233
+ cuvid_decoder = nil
1234
+ end
1235
+
1236
+ cuvid_options = []
1237
+
1238
+ if burn_subtitle.nil?
1239
+ overlay_filter = nil
1240
+ else
1241
+ overlay_filter = "[0:#{burn_subtitle['index']}]overlay"
1242
+
1243
+ unless cuvid_decoder.nil?
1244
+ Kernel.warn '**********'
1245
+ Kernel.warn "burning subtitle disables video decoder: #{cuvid_decoder}"
1246
+ Kernel.warn '**********'
1247
+ cuvid_decoder = nil
1248
+ end
1249
+ end
1250
+
1251
+ deinterlace = @deinterlace
1252
+ rate = @rate
1253
+
1254
+ if @enable_filters
1255
+ if video['avg_frame_rate'] == '30000/1001' or video['field_order'] != 'progressive'
1256
+ deinterlace = true
1257
+
1258
+ if video['codec_name'] == 'mpeg2video'
1259
+ rate = '24000/1001'
1260
+ end
1261
+ end
1262
+ end
1263
+
1264
+ frame_rate_filter = nil
1265
+
1266
+ if deinterlace
1267
+ if cuvid_decoder.nil?
1268
+ frame_rate_filter = 'yadif=deint=interlaced'
1269
+ else
1270
+ cuvid_options += ['-deint:v', 'adaptive']
1271
+ end
1272
+ end
1273
+
1274
+ unless rate.nil?
1275
+ frame_rate_filter = '' if frame_rate_filter.nil?
1276
+ frame_rate_filter += ',' unless frame_rate_filter.empty?
1277
+ frame_rate_filter += "fps=#{rate}"
1278
+ end
1279
+
1280
+ if @detelecine
1281
+ unless cuvid_decoder.nil?
1282
+ Kernel.warn '**********'
1283
+ Kernel.warn "detelecine disables video decoder: #{cuvid_decoder}"
1284
+ Kernel.warn '**********'
1285
+ cuvid_decoder = nil
1286
+ end
1287
+
1288
+ frame_rate_filter = 'fieldmatch=order=tff:combmatch=none,decimate'
1289
+ end
1290
+
1291
+ width = video['width'].to_i
1292
+ height = video['height'].to_i
1293
+
1294
+ if width == 720 and height == 576 and video['codec_name'] == 'mpeg2video'
1295
+ pal = true
1296
+ else
1297
+ pal = false
1298
+ end
1299
+
1300
+ if crop.nil? or (crop == {:width => width, :height => height, :x => 0, :y => 0})
1301
+ crop_filter = nil
1302
+ else
1303
+ media_width = width
1304
+ media_height = height
1305
+ width = crop[:width]
1306
+ height = crop[:height]
1307
+
1308
+ if cuvid_decoder.nil?
1309
+ crop_filter = "crop=#{width}:#{height}:#{crop[:x]}:#{crop[:y]}"
1310
+ else
1311
+ crop_filter = nil
1312
+ top = crop[:y]
1313
+ bottom = media_height - (top + height)
1314
+ left = crop[:x]
1315
+ right = media_width - (left + width)
1316
+ cuvid_options += ['-crop:v', "#{top}x#{bottom}x#{left}x#{right}"]
1317
+ end
1318
+ end
1319
+
1320
+ if @hevc
1321
+ max_width = @max_width
1322
+ max_height = @max_height
1323
+ else
1324
+ max_width = [@max_width, 1920].min
1325
+ max_height = [@max_height, 1080].min
1326
+ end
1327
+
1328
+ if video['sample_aspect_ratio'] = '1:1' and (width > max_width or height > max_height)
1329
+ scale = [(max_width.to_f / width), (max_height.to_f / height)].min
1330
+ width = ((width * scale).ceil / 2) * 2
1331
+ height = ((height * scale).ceil / 2) * 2
1332
+
1333
+ if cuvid_decoder.nil?
1334
+ scale_filter = "scale=#{width}:#{height}"
1335
+ scale_filter += ':flags=bicubic' unless overlay_filter.nil?
1336
+ else
1337
+ scale_filter = nil
1338
+ cuvid_options += ['-resize:v', "#{width}x#{height}"]
1339
+ end
1340
+ else
1341
+ scale_filter = nil
1342
+ end
1343
+
1344
+ if @encoder =~ /vaapi$/
1345
+ decode_options = ['-vaapi_device', '/dev/dri/renderD128']
1346
+ else
1347
+ decode_options = []
1348
+ end
1349
+
1350
+ if cuvid_decoder.nil?
1351
+ if (@decode_scope == :vc1 and video['codec_name'] == 'vc1') or @decode_scope == :all
1352
+ if @encoder =~ /vaapi$/
1353
+ decode_options = [
1354
+ '-hwaccel', 'vaapi',
1355
+ '-hwaccel_device', '/dev/dri/renderD128',
1356
+ '-hwaccel_output_format', 'vaapi'
1357
+ ]
1358
+ else
1359
+ decode_options += ['-hwaccel', 'auto']
1360
+ end
1361
+ end
1362
+ else
1363
+ Kernel.warn "video decoder = #{cuvid_decoder}"
1364
+
1365
+ decode_options += [
1366
+ '-c:v', cuvid_decoder
1367
+ ] + cuvid_options
1368
+ end
1369
+
1370
+ if @encoder =~ /vaapi$/ and not decode_options.include?('-hwaccel')
1371
+ conversion_filter = 'format=nv12,hwupload'
1372
+ else
1373
+ conversion_filter = nil
1374
+ end
1375
+
1376
+ filter = overlay_filter.nil? ? '' : overlay_filter
1377
+ filter += frame_rate_filter.nil? ? '' : ",#{frame_rate_filter}"
1378
+ filter += crop_filter.nil? ? '' : ",#{crop_filter}"
1379
+ filter += scale_filter.nil? ? '' : ",#{scale_filter}"
1380
+ filter += conversion_filter.nil? ? '' : ",#{conversion_filter}"
1381
+ filter.sub!(/^,/, '')
1382
+
1383
+ if overlay_filter.nil?
1384
+ encode_options = [
1385
+ '-map', "0:#{video['index']}"
1386
+ ]
1387
+
1388
+ unless filter.empty?
1389
+ encode_options += [
1390
+ '-filter:v', filter
1391
+ ]
1392
+ end
1393
+ else
1394
+ encode_options = [
1395
+ '-filter_complex', "[0:#{video['index']}]#{filter}[v]",
1396
+ '-map', '[v]'
1397
+ ]
1398
+ end
1399
+
1400
+ hdr = ((video.fetch('pix_fmt', 'yuv420p') == 'yuv420p10le') and @ten_bit)
1401
+
1402
+ if hdr
1403
+ color_primaries = 'bt2020'
1404
+ color_trc = 'smpte2084'
1405
+ colorspace = 'bt2020nc'
1406
+ else
1407
+ color_primaries = 'bt709'
1408
+ color_trc = 'bt709'
1409
+ colorspace = 'bt709'
1410
+ end
1411
+
1412
+ if width > 1920 or height > 1080
1413
+ bitrate = @target_2160p
1414
+ max_bitrate = 40000
1415
+ elsif width > 1280 or height > 720
1416
+ bitrate = @target_1080p
1417
+ max_bitrate = 20000
1418
+ elsif width > 720 or height > 576
1419
+ bitrate = @target_720p
1420
+ max_bitrate = 10000
1421
+ else
1422
+ bitrate = @target_480p
1423
+ max_bitrate = 5000
1424
+
1425
+ unless hdr
1426
+ color_primaries = pal ? 'bt470bg' : 'smpte170m'
1427
+ colorspace = 'smpte170m'
1428
+ end
1429
+ end
1430
+
1431
+ bitrate = @target unless @target.nil?
1432
+ bitrate = [bitrate, max_bitrate].min
1433
+ maxrate = bitrate * 3
1434
+
1435
+ if @preset.nil? or @preset == 'none'
1436
+ preset = nil
1437
+ else
1438
+ valid = false
1439
+
1440
+ case @encoder
1441
+ when /nvenc$/
1442
+ case @preset
1443
+ when 'fast', 'medium', 'slow'
1444
+ valid = true
1445
+ end
1446
+ when /qsv$/
1447
+ case @preset
1448
+ when 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'
1449
+ valid = true
1450
+ end
1451
+ when /^libx26[45]$/
1452
+ case @preset
1453
+ when 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium',
1454
+ 'slow', 'slower', 'veryslow', 'placebo'
1455
+ valid = true
1456
+ end
1457
+ end
1458
+
1459
+ fail "invalid preset for encoder: #{@preset}" unless valid
1460
+ preset = @preset
1461
+ end
1462
+
1463
+ Kernel.warn 'Stream mapping:'
1464
+ text = "#{sprintf("%2d", video['index'])} = #{@encoder} / #{bitrate} Kbps"
1465
+ text += " / #{preset}" unless preset.nil?
1466
+
1467
+ unless burn_subtitle.nil?
1468
+ text += " / #{sprintf("%d", burn_subtitle['index'])} = #{burn_subtitle['codec_name']} / burn"
1469
+ end
1470
+
1471
+ Kernel.warn text
1472
+ encode_options += ['-c:v', @encoder]
1473
+ encode_options += ['-pix_fmt:v', (@encoder =~ /(nvenc|qsv)$/ ? 'p010le' : 'yuv420p10le')] if @ten_bit
1474
+ encode_options += ['-b:v', "#{bitrate}k"]
1475
+ encode_options += ['-maxrate:v', "#{maxrate}k"] if @encoder =~ /(nvenc|hevc_qsv|libx26[45])$/
1476
+ encode_options += ['-bufsize:v', "#{maxrate}k"] if @encoder =~ /^libx26[45]$/
1477
+ encode_options += ['-preset:v', preset] unless preset.nil?
1478
+ encode_options += ['-allow_sw:v', '1'] if @encoder =~ /videotoolbox$/ and @vt_allow_sw
1479
+
1480
+ if @encoder =~ /nvenc$/
1481
+ spatial_aq = @nvenc_spatial_aq.nil? ? false : @nvenc_spatial_aq
1482
+ temporal_aq = @nvenc_temporal_aq.nil? ? false : @nvenc_temporal_aq
1483
+
1484
+ if @hevc
1485
+ spatial_aq_option = '-spatial_aq:v'
1486
+ temporal_aq_option = '-temporal_aq:v'
1487
+ else
1488
+ spatial_aq_option = '-spatial-aq:v'
1489
+ temporal_aq_option = '-temporal-aq:v'
1490
+ end
1491
+
1492
+ if @preset.nil?
1493
+ encode_options += ['-rc:v', 'vbr_hq']
1494
+ spatial_aq = true if @nvenc_spatial_aq.nil?
1495
+
1496
+ unless @hevc
1497
+ temporal_aq = true if @nvenc_temporal_aq.nil?
1498
+ end
1499
+ end
1500
+
1501
+ encode_options += [spatial_aq_option, '1'] if spatial_aq
1502
+ encode_options += [temporal_aq_option, '1'] if temporal_aq
1503
+ encode_options += ['-rc-lookahead:v', @nvenc_lookahead.to_s] unless @nvenc_lookahead.nil?
1504
+ encode_options += ['-refs:v', @nvenc_refs.to_s] unless @nvenc_refs.nil?
1505
+ encode_options += ['-bf:v', @nvenc_bframes.to_s] unless @nvenc_bframes.nil?
1506
+ end
1507
+
1508
+ if @encoder =~ /qsv$/
1509
+ encode_options += ['-look_ahead:v', '1'] if @encoder == 'h264_qsv'
1510
+ encode_options += ['-refs:v', @qsv_refs.to_s] unless @qsv_refs.nil?
1511
+ encode_options += ['-bf:v', @qsv_bframes.to_s] unless @qsv_bframes.nil?
1512
+ encode_options += ['-load_plugin:v', 'hevc_hw'] if @encoder == 'hevc_qsv'
1513
+ end
1514
+
1515
+ if @encoder =~ /amf$/
1516
+ encode_options += ['-rc:v', 'vbr_latency']
1517
+ encode_options += ['-quality:v', @amf_quality] unless @amf_quality.nil?
1518
+ encode_options += ['-enable_vbaq:v', '1'] if @amf_vbaq
1519
+ encode_options += ['-preanalysis:v', '1'] if @amf_pre_analysis
1520
+ encode_options += ['-refs:v', @amf_refs.to_s] unless @amf_refs.nil?
1521
+ encode_options += ['-bf:v', @amf_bframes.to_s] unless @amf_bframes.nil?
1522
+ end
1523
+
1524
+ if @encoder =~ /vaapi$/
1525
+ encode_options += ['-compression_level:v', @vaapi_compression.to_s] unless @vaapi_compression.nil?
1526
+ end
1527
+
1528
+ if @encoder == 'libx264'
1529
+ encode_options += ['-x264opts:v', 'ratetol=inf'] if @x264_avbr
1530
+ encode_options += ['-mbtree:v', '0']
1531
+
1532
+ if @preset.nil? and @x264_quick
1533
+ encode_options += [
1534
+ '-refs:v', '1',
1535
+ '-rc-lookahead:v', '30',
1536
+ '-partitions:v', 'none'
1537
+ ]
1538
+ end
1539
+ end
1540
+
1541
+ unless @ten_bit
1542
+ encode_options += ['-profile:v', 'high'] if @encoder =~ /^(h264_nvenc|h264_amf|libx264)$/
1543
+ end
1544
+
1545
+ encode_options += [
1546
+ '-color_primaries:v', color_primaries,
1547
+ '-color_trc:v', color_trc,
1548
+ '-colorspace:v', colorspace,
1549
+ '-metadata:s:v', 'title=',
1550
+ '-disposition:v', 'default'
1551
+ ]
1552
+
1553
+ [decode_options, encode_options]
1554
+ end
1555
+
1556
+ def get_audio_options(media_info)
1557
+ audio_track = 0
1558
+ main_audio = nil
1559
+
1560
+ media_info['streams'].each do |stream|
1561
+ next if stream['codec_type'] != 'audio'
1562
+
1563
+ audio_track += 1
1564
+
1565
+ if audio_track == @audio_selections[0][:track]
1566
+ main_audio = stream
1567
+ break
1568
+ end
1569
+ end
1570
+
1571
+ return ['-an'] if main_audio.nil?
1572
+
1573
+ width = @audio_selections[0][:width]
1574
+
1575
+ audio_tracks = [{
1576
+ :stream => main_audio,
1577
+ :width => width,
1578
+ :bitrate => width == :surround ? @surround_bitrate : @stereo_bitrate
1579
+ }]
1580
+
1581
+ titles = {}
1582
+ index = 0
1583
+
1584
+ @audio_selections.each do |selection|
1585
+ if index == 0
1586
+ index += 1
1587
+ next
1588
+ end
1589
+
1590
+ width = selection[:width]
1591
+ bitrate = width == :surround ? @surround_bitrate : @stereo_bitrate
1592
+
1593
+ unless selection[:track].nil?
1594
+ audio_track = 0
1595
+
1596
+ media_info['streams'].each do |stream|
1597
+ next if stream['codec_type'] != 'audio'
1598
+
1599
+ audio_track += 1
1600
+
1601
+ if audio_track == selection[:track]
1602
+ audio_tracks += [{
1603
+ :stream => stream,
1604
+ :width => width,
1605
+ :bitrate => bitrate
1606
+ }]
1607
+
1608
+ break
1609
+ end
1610
+ end
1611
+ end
1612
+
1613
+ unless selection[:language].nil?
1614
+ media_info['streams'].each do |stream|
1615
+ next if stream['codec_type'] != 'audio'
1616
+
1617
+ if stream.fetch('tags', {}).fetch('language', '') == selection[:language] and
1618
+ stream['index'] != main_audio['index']
1619
+ audio_tracks += [{
1620
+ :stream => stream,
1621
+ :width => width,
1622
+ :bitrate => bitrate
1623
+ }]
1624
+ end
1625
+ end
1626
+ end
1627
+
1628
+ unless selection[:title].nil?
1629
+ media_info['streams'].each do |stream|
1630
+ next if stream['codec_type'] != 'audio'
1631
+
1632
+ title = stream.fetch('tags', {}).fetch('title', '')
1633
+
1634
+ if title =~ /#{selection[:title]}/i and stream['index'] != main_audio['index']
1635
+ audio_tracks += [{
1636
+ :stream => stream,
1637
+ :width => width,
1638
+ :bitrate => bitrate
1639
+ }]
1640
+
1641
+ titles[stream['index']] = title
1642
+ end
1643
+ end
1644
+ end
1645
+
1646
+ index += 1
1647
+ end
1648
+
1649
+ audio_tracks.uniq!
1650
+ options = []
1651
+ configurations = {}
1652
+ index = 0
1653
+
1654
+ audio_tracks.each do |track|
1655
+ codec_name = track[:stream]['codec_name']
1656
+ input_channels = track[:stream]['channels'].to_i
1657
+ encoder = nil
1658
+ bitrate = nil
1659
+ channels = nil
1660
+
1661
+ if track[:width] == :surround
1662
+ if codec_name == @surround_encoder or codec_name == 'ac3'
1663
+ encoder = 'copy'
1664
+ elsif input_channels > 2
1665
+ encoder = @surround_encoder
1666
+ bitrate = @surround_bitrate
1667
+ end
1668
+ end
1669
+
1670
+ if encoder.nil?
1671
+ if input_channels <= 2 and (codec_name == 'aac' or
1672
+ ((codec_name == @surround_encoder or codec_name == 'ac3') and
1673
+ (track[:stream]['bit_rate'].to_i / 1000) <= @stereo_bitrate))
1674
+ encoder = 'copy'
1675
+ else
1676
+ encoder = @stereo_encoder
1677
+ bitrate = @stereo_bitrate
1678
+
1679
+ if input_channels > 2
1680
+ channels = 2
1681
+ elsif input_channels == 1
1682
+ bitrate = @stereo_bitrate / 2
1683
+ end
1684
+ end
1685
+ end
1686
+
1687
+ input_index = track[:stream]['index']
1688
+
1689
+ configuration = {
1690
+ :encoder => encoder,
1691
+ :bitrate => bitrate,
1692
+ :channels => channels
1693
+ }
1694
+
1695
+ next if configurations[input_index] == configuration
1696
+
1697
+ configurations[input_index] = configuration
1698
+ text = "#{sprintf("%2d", input_index)} = #{encoder}"
1699
+ text += " / #{bitrate} Kbps" unless bitrate.nil?
1700
+ text += ' / stereo' unless channels.nil?
1701
+ text += " / #{titles[input_index]}" if titles.has_key?(input_index)
1702
+ Kernel.warn text
1703
+ copy_track_name = (@copy_track_names or titles.has_key?(input_index))
1704
+
1705
+ options += [
1706
+ '-map', "0:#{input_index}",
1707
+ "-c:a:#{index}", encoder
1708
+ ] + (encoder == 'aac_at' ? ["-aac_at_mode:a:#{index}", 'cvbr'] : []) +
1709
+ (bitrate.nil? ? [] : ["-b:a:#{index}", "#{bitrate}k"]) +
1710
+ (channels.nil? ? [] : ["-ac:a:#{index}", "#{channels}"]) +
1711
+ (track[:stream]['sample_rate'] != '48000' ? ["-ar:a:#{index}", '48000'] : []) +
1712
+ (copy_track_name ? [] : ["-metadata:s:a:#{index}", 'title=']) + [
1713
+ "-disposition:a:#{index}", (index == 0 ? 'default' : '0')
1714
+ ]
1715
+
1716
+ index += 1
1717
+ end
1718
+
1719
+ options
1720
+ end
1721
+
1722
+ def get_subtitle_options(media_info, burn_subtitle)
1723
+ return ['-sn'] if @subtitle_selections.empty?
1724
+
1725
+ force_subtitle = nil
1726
+
1727
+ if @auto_add_subtitle
1728
+ media_info['streams'].each do |stream|
1729
+ next if stream['codec_type'] != 'subtitle'
1730
+
1731
+ if stream['disposition']['forced'] == 1
1732
+ force_subtitle = stream
1733
+ break
1734
+ end
1735
+ end
1736
+ end
1737
+
1738
+ subtitles = []
1739
+
1740
+ @subtitle_selections.each do |selection|
1741
+ unless selection[:track].nil?
1742
+ track = 0
1743
+
1744
+ media_info['streams'].each do |stream|
1745
+ next if stream['codec_type'] != 'subtitle'
1746
+
1747
+ track += 1
1748
+
1749
+ if track == selection[:track]
1750
+ if selection[:forced] and force_subtitle.nil?
1751
+ force_subtitle = stream
1752
+ else
1753
+ subtitles += [stream]
1754
+ end
1755
+
1756
+ break
1757
+ end
1758
+ end
1759
+ end
1760
+
1761
+ unless selection[:language].nil?
1762
+ media_info['streams'].each do |stream|
1763
+ next if stream['codec_type'] != 'subtitle'
1764
+
1765
+ if stream.fetch('tags', {}).fetch('language', '') == selection[:language]
1766
+ subtitles += [stream]
1767
+ end
1768
+ end
1769
+ end
1770
+
1771
+ unless selection[:title].nil?
1772
+ media_info['streams'].each do |stream|
1773
+ next if stream['codec_type'] != 'subtitle'
1774
+
1775
+ if stream.fetch('tags', {}).fetch('title', '') =~ /#{selection[:title]}/i
1776
+ subtitles += [stream]
1777
+ end
1778
+ end
1779
+ end
1780
+ end
1781
+
1782
+ unless force_subtitle.nil?
1783
+ subtitles = [force_subtitle] + subtitles
1784
+ end
1785
+
1786
+ subtitles.uniq!
1787
+ options = []
1788
+ index = 0
1789
+
1790
+ subtitles.each do |subtitle|
1791
+ next if (not burn_subtitle.nil?) and burn_subtitle['index'] == subtitle['index']
1792
+
1793
+ next if @format == :mp4 and
1794
+ (subtitle['codec_name'] == 'hdmv_pgs_subtitle' or subtitle['codec_name'] == 'dvd_subtitle')
1795
+
1796
+ force = (index == 0 and not force_subtitle.nil?)
1797
+ text = "#{sprintf("%2d", subtitle['index'])} = #{subtitle['codec_name']}"
1798
+ text += ' / force' if force
1799
+ title = subtitle.fetch('tags', {}).fetch('title', '')
1800
+ text += " / #{title}" unless title.empty?
1801
+ Kernel.warn text
1802
+
1803
+ options += [
1804
+ '-map', "0:#{subtitle['index']}",
1805
+ "-c:s:#{index}", 'copy',
1806
+ "-disposition:s:#{index}", (force ? 'default+forced' : '0')
1807
+ ]
1808
+
1809
+ index += 1
1810
+ end
1811
+
1812
+ return ['-sn'] if options.empty?
1813
+
1814
+ options
1815
+ end
1816
+
1817
+ def assemble_log(log_path, output)
1818
+ Kernel.warn 'Assembling `.log` file...'
1819
+ content = ''
1820
+
1821
+ begin
1822
+ content = File.read(log_path)
1823
+ rescue SystemCallError => e
1824
+ raise "reading `.log` file failed: #{e}"
1825
+ end
1826
+
1827
+ begin
1828
+ log_file = File.new(log_path, 'wb')
1829
+ log_file.print content
1830
+ .gsub(/^.*Warning during encoding.*\R/, '')
1831
+ .gsub(/^.*dropping frame [0-9]+ from stream.*\R/, '')
1832
+ log_file.puts 'Stats:'
1833
+ log_file.print output.gsub(/^.*\r(.)/, '\1')
1834
+ log_file.close
1835
+ rescue SystemCallError => e
1836
+ raise "writing `.log` file failed: #{e}"
1837
+ end
1838
+ end
1839
+
1840
+ def add_track_statistics_tags(output_path)
1841
+ Kernel.warn 'Adding track statistics...'
1842
+
1843
+ begin
1844
+ IO.popen(['mkvpropedit', output_path, '--add-track-statistics-tags'], 'rb') do |io|
1845
+ Signal.trap 'INT' do
1846
+ Process.kill 'INT', io.pid
1847
+ end
1848
+
1849
+ io.each_char do |char|
1850
+ STDERR.print char
1851
+ end
1852
+ end
1853
+ rescue SystemCallError => e
1854
+ raise "adding track statistics tags failed: #{e}"
1855
+ end
1856
+
1857
+ fail "adding track statistics tags failed: #{output_path}" unless $CHILD_STATUS.exitstatus == 0
1858
+ end
1859
+ end
1860
+ end
1861
+
1862
+ Transcoding::Command.new.run