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.
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