other_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,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