video_transcoding 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/convert-video ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env ruby -W
2
+ #
3
+ # convert-video
4
+ #
5
+ # Copyright (c) 2013-2015 Don Melton
6
+ #
7
+
8
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
9
+
10
+ require 'video_transcoding/cli'
11
+
12
+ module VideoTranscoding
13
+ class Command
14
+ include CLI
15
+
16
+ def about
17
+ <<HERE
18
+ convert-video #{VERSION}
19
+ #{COPYRIGHT}
20
+ HERE
21
+ end
22
+
23
+ def usage
24
+ <<HERE
25
+ Convert video file from Matroska to MP4 format or from MP4 to Matroksa format
26
+ WITHOUT TRANSCODING VIDEO.
27
+
28
+ Usage: #{$PROGRAM_NAME} [OPTION]... [FILE]...
29
+
30
+ -o, --output DIRECTORY
31
+ set output path
32
+ (default: input filename with output format extension
33
+ in current working directory)
34
+ --use-m4v use `.m4v` extension instead of `.mp4` for MP4 output
35
+
36
+ -v, --verbose increase diagnostic information
37
+ -q, --quiet decrease " "
38
+
39
+ -h, --help display this help and exit
40
+ --version output version information and exit
41
+
42
+ Requires `HandBrakeCLI`, `mp4track`, `ffmpeg` and `mkvmerge`.
43
+ HERE
44
+ end
45
+
46
+ def initialize
47
+ super
48
+ @output = nil
49
+ @use_m4v = false
50
+ end
51
+
52
+ def define_options(opts)
53
+ opts.on('-o', '--output ARG') { |arg| @output = arg }
54
+ opts.on('--use-m4v') { @use_m4v = true }
55
+ end
56
+
57
+ def configure
58
+ unless @output.nil? or File.directory? @output
59
+ fail UsageError, "not a directory: #{@output}"
60
+ end
61
+
62
+ HandBrake.setup
63
+ MP4track.setup
64
+ FFmpeg.setup
65
+ MKVmerge.setup
66
+ end
67
+
68
+ def process_input(arg)
69
+ Console.info "Processing: #{arg}..."
70
+ media = Media.new(path: arg, allow_directory: false)
71
+ Console.debug media.info
72
+ output = resolve_output(media)
73
+ fail "no H.264 format video track: #{arg}" unless media.info[:h264]
74
+ fail "no video stream: #{arg}" unless media.info.has_key? :stream
75
+
76
+ if media.info[:mkv]
77
+ convert_to_mp4(media, output)
78
+ else
79
+ convert_to_mkv(media, output)
80
+ end
81
+ end
82
+
83
+ def resolve_output(media)
84
+ if media.info[:mkv]
85
+ ext = @use_m4v ? '.m4v' : '.mp4'
86
+ elsif media.info[:mp4]
87
+ ext = '.mkv'
88
+ else
89
+ fail "unsupported input container format: #{media.path}"
90
+ end
91
+
92
+ output = File.basename(media.path, '.*') + ext
93
+
94
+ unless @output.nil?
95
+ output = File.absolute_path(@output) + File::SEPARATOR + output
96
+ end
97
+
98
+ fail "output file exists: #{media.path}" if File.exist? output
99
+ output
100
+ end
101
+
102
+ def convert_to_mp4(media, output)
103
+ map_options = [
104
+ '-map', "0:#{media.info[:stream]}"
105
+ ]
106
+ copy_options = [
107
+ '-c:v', 'copy'
108
+ ]
109
+
110
+ unless media.info[:audio].empty?
111
+ track_order = media.info[:audio].keys
112
+ stream = 0
113
+
114
+ if media.info[:audio][1][:channels] > 2.0
115
+ if track_order.size > 1
116
+ if media.info[:audio][1][:language] == media.info[:audio][2][:language] and
117
+ media.info[:audio][2][:format] == 'AAC' and
118
+ media.info[:audio][2][:channels] <= 2.0
119
+ first = track_order[0]
120
+ track_order[0] = track_order[1]
121
+ track_order[1] = first
122
+ end
123
+ else
124
+ map_options.concat([
125
+ '-map', "0:#{media.info[:audio][1][:stream]}"
126
+ ])
127
+
128
+ if FFmpeg.aac_encoder == 'aac'
129
+ copy_options.concat([
130
+ '-strict', 'experimental'
131
+ ])
132
+ end
133
+
134
+ copy_options.concat([
135
+ '-ac', '2',
136
+ '-c:a:0', FFmpeg.aac_encoder,
137
+ '-b:a:0', '160k'
138
+ ])
139
+ stream += 1
140
+ end
141
+ end
142
+
143
+ track_order.each do |track|
144
+ map_options.concat([
145
+ '-map', "0:#{media.info[:audio][track][:stream]}"
146
+ ])
147
+
148
+ if media.info[:audio][track][:channels] > 2.0 and
149
+ media.info[:audio][track][:format] != 'AC3'
150
+ copy_options.concat([
151
+ '-ac', '6',
152
+ "-c:a:#{stream}", 'ac3',
153
+ "-b:a:#{stream}", '384k'
154
+ ])
155
+ elsif media.info[:audio][track][:channels] <= 2.0 and
156
+ media.info[:audio][track][:format] != 'AAC' and
157
+ media.info[:audio][track][:format] != 'AC3'
158
+ if FFmpeg.aac_encoder == 'aac'
159
+ copy_options.concat([
160
+ '-strict', 'experimental'
161
+ ])
162
+ end
163
+
164
+ copy_options.concat([
165
+ '-ac', '2',
166
+ "-c:a:#{stream}", FFmpeg.aac_encoder,
167
+ "-b:a:#{stream}", '160k'
168
+ ])
169
+ else
170
+ copy_options.concat([
171
+ "-c:a:#{stream}", 'copy'
172
+ ])
173
+ end
174
+
175
+ stream += 1
176
+ end
177
+ end
178
+
179
+ ffmpeg_command = [
180
+ FFmpeg.command_name,
181
+ '-hide_banner',
182
+ '-nostdin',
183
+ '-i', media.path,
184
+ *map_options,
185
+ *copy_options,
186
+ output
187
+ ]
188
+ Console.debug ffmpeg_command
189
+ Console.info 'Converting with ffmpeg...'
190
+
191
+ begin
192
+ IO.popen(ffmpeg_command, :err=>[:child, :out]) do |io|
193
+ Signal.trap 'INT' do
194
+ Process.kill 'INT', io.pid
195
+ end
196
+
197
+ io.each_char do |char|
198
+ print char
199
+ end
200
+ end
201
+ rescue SystemCallError => e
202
+ raise "conversion failed: #{e}"
203
+ end
204
+
205
+ fail "conversion failed: #{media.path}" unless $CHILD_STATUS.exitstatus == 0
206
+ mp4_media = Media.new(path: output, allow_directory: false)
207
+ Console.debug media.info
208
+
209
+ mp4_media.info[:audio].each do |track, info|
210
+ if track == 1 and not info[:default]
211
+ enabled = 'true'
212
+ elsif track != 1 and info[:default]
213
+ enabled = 'false'
214
+ else
215
+ enabled = nil
216
+ end
217
+
218
+ unless enabled.nil?
219
+ begin
220
+ IO.popen([
221
+ MP4track.command_name,
222
+ '--track-index', track.to_s,
223
+ '--enabled', enabled,
224
+ output,
225
+ ], :err=>[:child, :out]) do |io|
226
+ io.each do |line|
227
+ Console.debug line
228
+ end
229
+ end
230
+ rescue SystemCallError => e
231
+ raise "adjusting audio enabled failed: #{e}"
232
+ end
233
+
234
+ fail "adjusting audio enabled failed: #{output}" unless $CHILD_STATUS.exitstatus == 0
235
+ end
236
+ end
237
+ end
238
+
239
+ def convert_to_mkv(media, output)
240
+ track_order = ['0:' + media.info[:stream].to_s]
241
+ track_name_options = []
242
+
243
+ media.info[:audio].each do |_, info|
244
+ track_order << '0:' + info[:stream].to_s
245
+ track_name_options.concat([
246
+ '--track-name', "#{info[:stream]}:#{info[:name]}"
247
+ ])
248
+ end
249
+
250
+ if track_order.size > 2 and
251
+ media.info[:audio][1][:language] == media.info[:audio][2][:language] and
252
+ media.info[:audio][1][:format] == 'AAC' and
253
+ media.info[:audio][1][:channels] <= 2.0 and
254
+ media.info[:audio][2][:channels] > 2.0
255
+ first = track_order[1]
256
+ track_order[1] = track_order[2]
257
+ track_order[2] = first
258
+ end
259
+
260
+ mkvmerge_command = [
261
+ MKVmerge.command_name,
262
+ '--output', output,
263
+ '--track-order', track_order.join(','),
264
+ '--disable-track-statistics-tags'
265
+ ]
266
+ mkvmerge_command += track_name_options unless track_name_options.empty?
267
+ mkvmerge_command << media.path
268
+ Console.debug mkvmerge_command
269
+ Console.info 'Converting with mkvmerge...'
270
+
271
+ begin
272
+ IO.popen(mkvmerge_command, :err=>[:child, :out]) do |io|
273
+ Signal.trap 'INT' do
274
+ Process.kill 'INT', io.pid
275
+ end
276
+
277
+ io.each_char do |char|
278
+ print char
279
+ end
280
+ end
281
+ rescue SystemCallError => e
282
+ raise "conversion failed: #{e}"
283
+ end
284
+
285
+ fail "conversion failed: #{media.path}" if $CHILD_STATUS.exitstatus == 2
286
+ end
287
+ end
288
+ end
289
+
290
+ VideoTranscoding::Command.new.run
data/bin/detect-crop ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby -W
2
+ #
3
+ # detect-crop
4
+ #
5
+ # Copyright (c) 2013-2015 Don Melton
6
+ #
7
+
8
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
9
+
10
+ require 'video_transcoding/cli'
11
+
12
+ module VideoTranscoding
13
+ class Command
14
+ include CLI
15
+
16
+ def about
17
+ <<HERE
18
+ detect-crop #{VERSION}
19
+ #{COPYRIGHT}
20
+ HERE
21
+ end
22
+
23
+ def usage
24
+ <<HERE
25
+ Detect optimal crop values for video file or disc image directory.
26
+
27
+ Usage: #{$PROGRAM_NAME} [OPTION]... [FILE|DIRECTORY]...
28
+
29
+ --scan list title(s) and tracks in video media and exit
30
+ --title NUMBER select numbered title in video media
31
+ (default: main feature or first listed)
32
+ --no-constrain don't constrain crop to optimal shape
33
+ --values-only output only unambiguous crop values, not commands
34
+
35
+ -v, --verbose increase diagnostic information
36
+ -q, --quiet decrease " "
37
+
38
+ -h, --help display this help and exit
39
+ --version output version information and exit
40
+
41
+ Requires `HandBrakeCLI` and `mplayer`.
42
+ HERE
43
+ end
44
+
45
+ def initialize
46
+ super
47
+ @scan = false
48
+ @title = nil
49
+ @constrain = true
50
+ @values_only = false
51
+ end
52
+
53
+ def define_options(opts)
54
+ opts.on('--scan') { @scan = true }
55
+ opts.on('--title ARG', Integer) { |arg| @title = arg }
56
+ opts.on('--no-constrain') { @constrain = false }
57
+ opts.on('--values-only') { @values_only = true }
58
+ end
59
+
60
+ def configure
61
+ HandBrake.setup
62
+ MPlayer.setup
63
+ end
64
+
65
+ def process_input(arg)
66
+ Console.info "Processing: #{arg}..."
67
+
68
+ if @scan
69
+ media = Media.new(path: arg, title: @title)
70
+ Console.debug media.info
71
+ puts media.summary
72
+ return
73
+ end
74
+
75
+ media = Media.new(path: arg, title: @title, autocrop: true, extended: false)
76
+ Console.debug media.info
77
+ width, height = media.info[:width], media.info[:height]
78
+ directory = media.info[:directory]
79
+ hb_crop = media.info[:autocrop]
80
+ hb_crop = Crop.constrain(hb_crop, width, height) if @constrain
81
+ shell_path = arg.shellescape
82
+
83
+ print_transcode = ->(crop) do
84
+ puts
85
+ print 'transcode-video '
86
+ print "--title #{media.info[:title]} " if directory
87
+ puts "--crop #{Crop.handbrake_string(crop)} #{shell_path}"
88
+ puts
89
+ end
90
+
91
+ print_all = ->(crop) do
92
+ str = Crop.mplayer_string(crop, width, height)
93
+ puts
94
+ puts "mplayer -really-quiet -nosound -vf rectangle=#{str} #{shell_path}"
95
+ puts "mplayer -really-quiet -nosound -vf crop=#{str} #{shell_path}"
96
+ print_transcode.call crop
97
+ end
98
+
99
+ if directory
100
+ if @values_only
101
+ puts Crop.handbrake_string(hb_crop)
102
+ else
103
+ print_transcode.call hb_crop
104
+ end
105
+ else
106
+ mp_crop = Crop.detect(arg, media.info[:duration], width, height)
107
+ mp_crop = Crop.constrain(mp_crop, width, height) if @constrain
108
+
109
+ if hb_crop == mp_crop
110
+ if @values_only
111
+ puts Crop.handbrake_string(hb_crop)
112
+ else
113
+ Console.info 'Results from HandBrakeCLI and mplayer are identical...'
114
+ print_all.call hb_crop
115
+ end
116
+ else
117
+ fail "results from HandBrakeCLI and mplayer differ: #{arg}" if @values_only
118
+ Console.warn 'Results differ...'
119
+ puts
120
+ puts '# From HandBrakeCLI:'
121
+ print_all.call hb_crop
122
+ puts '# From mplayer:'
123
+ print_all.call mp_crop
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ VideoTranscoding::Command.new.run
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env ruby -W
2
+ #
3
+ # query-handbrake-log
4
+ #
5
+ # Copyright (c) 2013-2015 Don Melton
6
+ #
7
+
8
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
9
+
10
+ require 'abbrev'
11
+ require 'video_transcoding/cli'
12
+
13
+ module VideoTranscoding
14
+ class Command
15
+ include CLI
16
+
17
+ def about
18
+ <<HERE
19
+ query-handbrake-log #{VERSION}
20
+ #{COPYRIGHT}
21
+ HERE
22
+ end
23
+
24
+ def usage
25
+ <<HERE
26
+ Report information from HandBrake-generated `.log` files.
27
+
28
+ Usage: #{$PROGRAM_NAME} INFO [OPTION]... [FILE|DIRECTORY]...
29
+
30
+ Information types:
31
+ t, time time spent during transcoding
32
+ (sorted from short to long)
33
+ s, speed speed of transcoding in frames per second
34
+ (sorted from fast to slow)
35
+ b, bitrate video bitrate of transcoded output
36
+ (sorted from low to high)
37
+ r, ratefactor average P-frame quantizer for transcoding
38
+ (sorted from low to high)
39
+
40
+ Options:
41
+ --reverse reverse direction of sort
42
+
43
+ -v, --verbose increase diagnostic information
44
+ -q, --quiet decrease " "
45
+
46
+ -h, --help display this help and exit
47
+ --version output version information and exit
48
+ HERE
49
+ end
50
+
51
+ def initialize
52
+ super
53
+ @reverse = false
54
+ @info = nil
55
+ @logs = []
56
+ @paths = []
57
+ end
58
+
59
+ def define_options(opts)
60
+ opts.on('--reverse') { @reverse = true }
61
+ end
62
+
63
+ def configure
64
+ fail UsageError, 'missing argument' if ARGV.empty?
65
+ arg = $ARGV.shift
66
+
67
+ @info = case arg
68
+ when 'time', 't'
69
+ :time
70
+ when 'speed', 's'
71
+ @reverse = !@reverse
72
+ :speed
73
+ when 'bitrate', 'b'
74
+ :bitrate
75
+ when 'ratefactor', 'r'
76
+ :ratefactor
77
+ else
78
+ fail UsageError, "invalid information type: #{arg}"
79
+ end
80
+ end
81
+
82
+ def process_input(arg)
83
+ Console.info "Processing: #{arg}..."
84
+ input = File.absolute_path(arg)
85
+
86
+ if File.directory? input
87
+ logs = Dir[input + File::SEPARATOR + '*.log']
88
+ fail "does not contain `.log` files: #{input}" if logs.empty?
89
+ @logs += logs
90
+ @paths << input
91
+ else
92
+ fail "not a `.log` file: #{input}" unless File.extname(input) == '.log'
93
+ @logs << File.absolute_path(input)
94
+ @paths << File.dirname(input)
95
+ end
96
+ end
97
+
98
+ def complete
99
+ @logs.uniq!
100
+ @paths.uniq!
101
+
102
+ if @paths.size > 1
103
+ prefix = File.dirname(@paths.abbrev.keys.min_by { |key| key.size }) + File::SEPARATOR
104
+ else
105
+ prefix = ''
106
+ end
107
+
108
+ report = []
109
+
110
+ @logs.each do |log|
111
+ Console.info "Reading: #{log}..."
112
+ video = File.basename(log, '.log')
113
+ video += " (#{File.dirname(log).sub(prefix, '')})" unless prefix.empty?
114
+ found = false
115
+ count = 0
116
+ duration_line = nil
117
+ rate_line = nil
118
+ fps_line = nil
119
+ fps_line_2 = nil
120
+ ratefactor_line = nil
121
+ bitrate_line = nil
122
+
123
+ begin
124
+ File.foreach(log) do |line|
125
+ found = true if not found and line =~ /^HandBrake/
126
+ count += 1
127
+ fail "not a HandBrake-generated `.log` file: #{log}" if count > 5 and not found
128
+ duration_line = line if duration_line.nil? and line =~ /\+ duration: /
129
+ rate_line = line if line =~ /\+ frame rate: /
130
+
131
+ if line =~ /average encoding speed/
132
+ if fps_line.nil?
133
+ fps_line = line
134
+ elsif fps_line_2.nil?
135
+ fps_line_2 = line
136
+ end
137
+ end
138
+
139
+ ratefactor_line = line if line =~ /x26[45] \[info\]: frame P:/
140
+ bitrate_line = line if bitrate_line.nil? and line =~ /mux: track 0/
141
+ end
142
+ rescue SystemCallError => e
143
+ raise "reading failed: #{e}"
144
+ end
145
+
146
+ fail "not a HandBrake-generated `.log` file: #{log}" unless found
147
+
148
+ case @info
149
+ when :time, :speed
150
+ if fps_line.nil?
151
+ if @info == :time
152
+ report << "00:00:00 #{video}"
153
+ else
154
+ report << "00.000000 fps #{video}"
155
+ end
156
+ else
157
+ Console.debug fps_line
158
+
159
+ if fps_line =~ / ([0-9.]+) fps$/
160
+ fps = $1
161
+ else
162
+ fail "fps not found: #{log}"
163
+ end
164
+
165
+ unless fps_line_2.nil?
166
+ Console.debug fps_line_2
167
+
168
+ if fps_line_2 =~ / ([0-9.]+) fps$/
169
+ fps_2 = $1
170
+ fps = (1 / ((1 / fps.to_f) + (1 / fps_2.to_f))).round(6).to_s
171
+ else
172
+ fail "fps not found: #{log}"
173
+ end
174
+ end
175
+
176
+ if @info == :time
177
+ fail "duration not found: #{log}" if duration_line.nil?
178
+ Console.debug duration_line
179
+
180
+ if duration_line =~ /([0-9]{2}):([0-9]{2}):([0-9]{2})/
181
+ duration = ($1.to_i * 60 * 60) + ($2.to_i * 60) + $3.to_i
182
+ fail "frame rate not found: #{log}" if rate_line.nil?
183
+ Console.debug rate_line
184
+ rate = rate_line.sub(/^.*: /, '').sub(/^.*constant /, '').sub(/ fps.*$/, '').to_f
185
+ seconds = ((duration * rate) / fps.to_f).to_i
186
+ hours = seconds / (60 * 60)
187
+ minutes = (seconds / 60) % 60
188
+ seconds = seconds % 60
189
+ report << format("%02d:%02d:%02d %s", hours, minutes, seconds, video)
190
+ else
191
+ fail "duration not found: #{log}"
192
+ end
193
+ # TODO
194
+ else
195
+ report << "#{fps} fps #{video}"
196
+ end
197
+ end
198
+ when :bitrate
199
+ if bitrate_line.nil?
200
+ report << "0000.00 kbps #{video}"
201
+ else
202
+ Console.debug bitrate_line
203
+
204
+ if bitrate_line =~ /[0-9.]+ kbps/
205
+ report << "#{$MATCH} #{video}"
206
+ else
207
+ fail "bitrate not found: #{log}"
208
+ end
209
+ end
210
+ when :ratefactor
211
+ if ratefactor_line.nil?
212
+ report << "00.00 #{video}"
213
+ else
214
+ Console.debug ratefactor_line
215
+
216
+ if ratefactor_line =~ /Avg QP:([0-9.]+)/
217
+ report << "#{$1} #{video}"
218
+ else
219
+ fail "ratefactor not found: #{log}"
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ if @info == :time
226
+ report.sort!
227
+ else
228
+ report.sort! do |a, b|
229
+ number_a = a.sub(/ .*$/, '').to_f
230
+ number_b = b.sub(/ .*$/, '').to_f
231
+
232
+ if number_a < number_b
233
+ -1
234
+ elsif number_a > number_b
235
+ 1
236
+ else
237
+ a <=> b
238
+ end
239
+ end
240
+ end
241
+
242
+ report.reverse! if @reverse
243
+ puts report
244
+ end
245
+ end
246
+ end
247
+
248
+ VideoTranscoding::Command.new.run