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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +19 -0
- data/README.md +108 -0
- data/bin/ask-ffmpeg-log +181 -0
- data/bin/other-transcode +1862 -0
- data/other_video_transcoding.gemspec +16 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/bin/ask-ffmpeg-log
ADDED
@@ -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
|
data/bin/other-transcode
ADDED
@@ -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
|