-
Notifications
You must be signed in to change notification settings - Fork 164
Expand file tree
/
Copy pathconvert-video.rb
More file actions
224 lines (181 loc) · 5.11 KB
/
convert-video.rb
File metadata and controls
224 lines (181 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env ruby
#
# convert-video.rb
#
# Copyright (c) 2025 Lisa Melton
#
require 'English'
require 'json'
require 'optparse'
module Converting
class UsageError < RuntimeError
end
class Command
def about
<<-HERE
convert-video.rb 2025.01.28
Copyright (c) 2025 Lisa Melton
HERE
end
def usage
<<-HERE
Convert media file from Matroska to MP4 format or other media to MKV
without transcoding.
Usage: #{File.basename($PROGRAM_NAME)} [OPTION]... [FILE]...
All video, audio and subtitle tracks are copied during conversion.
But incompatible subtitles are ignored when converting to MP4 format.
Options:
--debug increase diagnostic information
-n, --dry-run don't transcode, just show `ffmpeg` command
-h, --help display this help and exit
--version output version information and exit
Requires `ffmpeg` and `ffprobe`.
HERE
end
def initialize
@debug = false
@dry_run = false
end
def run
begin
OptionParser.new do |opts|
define_options opts
opts.on '-h', '--help' do
puts usage
exit
end
opts.on '--version' do
puts about
exit
end
end.parse!
rescue OptionParser::ParseError => e
raise UsageError, e
end
fail UsageError, 'missing argument' if ARGV.empty?
ARGV.each { |arg| process_input arg }
exit
rescue UsageError => e
Kernel.warn "#{$PROGRAM_NAME}: #{e}"
Kernel.warn "Try `#{File.basename($PROGRAM_NAME)} --help` for more information."
exit false
rescue StandardError => e
Kernel.warn "#{$PROGRAM_NAME}: #{e}"
exit(-1)
rescue SignalException
puts
exit(-1)
end
def define_options(opts)
opts.on '--debug' do
@debug = true
end
opts.on '-n', '--dry-run' do
@dry_run = true
end
end
def process_input(path)
seconds = Time.now.tv_sec
media_info = scan_media(path)
options = ['-c:v', 'copy', '-c:a', 'copy']
if media_info['format']['format_name'] =~ /matroska/
extension = '.mp4'
index = 0
media_info['streams'].each do |stream|
map_stream = false
codec_name = nil
case stream['codec_type']
when 'video', 'audio'
map_stream = true
when 'subtitle'
case stream['codec_name']
when 'dvd_subtitle'
map_stream = true
codec_name = 'copy'
when 'subrip'
map_stream = true
codec_name = 'mov_text'
else
Kernel.warn "Ignoring subtitle track \##{index + 1}"
end
index += 1
end
options += ['-map', "0:#{stream['index']}"] if map_stream
options += ["-c:s:#{index - 1}", codec_name] unless codec_name.nil?
end
options += ['-movflags', 'disable_chpl']
else
extension = '.mkv'
options += ['-c:s', 'copy']
end
output = File.basename(path, '.*') + extension
ffmpeg_command = [
'ffmpeg',
'-loglevel', (@debug ? 'verbose' : 'error'),
'-stats',
'-i', path,
*options,
output
]
command_line = escape_command(ffmpeg_command)
Kernel.warn 'Command line:'
if @dry_run
puts command_line
return
end
Kernel.warn command_line
fail "output file already exists: #{output}" if File.exist? output
Kernel.warn 'Converting...'
system(*ffmpeg_command, exception: true)
Kernel.warn "\nElapsed time: #{seconds_to_time(Time.now.tv_sec - seconds)}\n\n"
end
def scan_media(path)
Kernel.warn 'Scanning media...'
media_info = ''
IO.popen([
'ffprobe',
'-loglevel', 'quiet',
'-show_streams',
'-show_format',
'-print_format', 'json',
path
]) do |io|
media_info = io.read
end
fail "scanning media failed: #{path}" unless $CHILD_STATUS.exitstatus == 0
begin
media_info = JSON.parse(media_info)
rescue JSON::JSONError
fail "media information not found: #{path}"
end
Kernel.warn media_info.inspect if @debug
media_info
end
def escape_command(command)
command_line = ''
command.each {|item| command_line += "#{escape_string(item)} " }
command_line.sub!(/ $/, '')
command_line
end
def escape_string(str)
# See: https://github.com/larskanis/shellwords
return '""' if str.empty?
str = str.dup
if RUBY_PLATFORM =~ /mingw/
str.gsub!(/((?:\\)*)"/) { "\\" * ($1.length * 2) + "\\\"" }
if str =~ /\s/
str.gsub!(/(\\+)\z/) { "\\" * ($1.length * 2 ) }
str = "\"#{str}\""
end
else
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")
str.gsub!(/\n/, "'\n'")
end
str
end
def seconds_to_time(seconds)
sprintf("%02d:%02d:%02d", seconds / (60 * 60), (seconds / 60) % 60, seconds % 60)
end
end
end
Converting::Command.new.run