ffmpeg_progress 1.0.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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.rdoc +29 -0
  4. data/lib/ffmpeg_progress.rb +253 -0
  5. metadata +48 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: be912ce3dcef0d9351c5499cee46cae82a6d6f02
4
+ data.tar.gz: 2fd3d86091bc2c7e6b26e802a6f0a500a0ffaed3
5
+ SHA512:
6
+ metadata.gz: fa668255e546bc6905bba6acbd365d98fcba0dfd168c34c4e7172d2d9c6e627e648872827a590252747d13412d60c45b2f8363a3b6e24555a20a19231d6cc3f4
7
+ data.tar.gz: 5aa94f631e4848560fb61f05e328acef07b73608e23012c6171930108ff91c321272ff091291ae2a6d5090e65370280e8c2e2d4b6b1b68c93ab490ab6afa1adb
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Winterbraid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,29 @@
1
+ =FfmpegProgress
2
+
3
+ Execute +ffmpeg+ commands while displaying a pretty progress bar instead of the
4
+ usual garbage.
5
+
6
+ Convert from the shell with default options:
7
+ # Will save to ./converted/test.mp4
8
+ ruby -rffmpeg_progress -e "FfmpegProgress::Ffmpeg.new('test.avi').run"
9
+
10
+ ==Example Use
11
+ require 'ffmpeg_progress'
12
+
13
+ include FfmpegProgress
14
+
15
+ f = Ffmpeg.new 'input.avi'
16
+
17
+ # Occurrences of '_INPUT_' will be replaced with the input file name.
18
+ f.options = '-y -threads 0 -c:v libx264 -crf 22 -preset medium -c:a copy'
19
+
20
+ # Directories will be created if necessary.
21
+ f.output = 'output.mkv'
22
+
23
+ f.command
24
+ # => "ffmpeg -i input.avi -y -threads 0 -c:v libx264 -crf 22 " \
25
+ # "-preset medium -c:a copy output.mkv &> ffmpeg.log"
26
+
27
+ # The optional block is passed self as a parameter and appended to
28
+ # the progress bar.
29
+ f.run { |ffmpeg| ffmpeg.output }
@@ -0,0 +1,253 @@
1
+ require 'fileutils'
2
+
3
+ # {include:file:README.rdoc}
4
+ module FfmpegProgress
5
+ # The current software version.
6
+ VERSION = '1.0.0'
7
+
8
+ # The date of the current version.
9
+ DATE = '2014-10-31'
10
+
11
+ # A short description of the software.
12
+ ABOUT = 'A fancy progress bar for ffmpeg.'
13
+
14
+ # Contains various examples of +ffmpeg+ options.
15
+ module Presets
16
+ # The default options. Convert the video to x264 and audio to AAC.
17
+ DEFAULT_OPTS = '-y -threads 0 -strict -2 -c:a aac -b:a 96k ' \
18
+ '-c:v libx264 -preset fast -tune fastdecode -crf 22 '
19
+
20
+ # Read the default subtitles embedded in an input MKV file and hardcode
21
+ # them into the video. Any embedded fonts must be extracted and installed
22
+ # first to preserve them in the output, for example:
23
+ # mkvextract attachments input.mkv {1..10}
24
+ # cp *.ttf *.ttc *.otf ~/.fonts
25
+ BURN_MKV_SUBS = '-y -threads 0 -strict -2 -sn -c:a copy ' \
26
+ '-c:v libx264 -preset fast -tune fastdecode -crf 22 ' \
27
+ '-vf "subtitles=_INPUT_"'
28
+
29
+ # Like {BURN_MKV_SUBS}, but also downscale the video to 800px width.
30
+ BURN_800 = '-y -threads 0 -strict -2 -sn -c:a aac -b:a 96k ' \
31
+ '-c:v libx264 -preset fast -tune fastdecode -crf 22 ' \
32
+ '-vf "[in]scale=800:-2[tmp];[tmp]subtitles=_INPUT_[out]"'
33
+ end
34
+
35
+ # Helper methods.
36
+ module Utils
37
+ # Colorize a string for the terminal (256-color mode).
38
+ #
39
+ # @param [String] string
40
+ # @param [Integer] color
41
+ # @return [String]
42
+ def colorize(string, color)
43
+ "\x1b[38;5;#{color}m#{string}\x1b[0m"
44
+ end
45
+
46
+ # Parse a time string in the +ffmpeg+ HH:MM:SS.ms format and return seconds.
47
+ #
48
+ # @param [String] time_string
49
+ # @return [Integer]
50
+ def parse_ffmpeg_time(time_string)
51
+ array = time_string.rpartition('.').first.split(':').map(&:to_i)
52
+
53
+ array[0] * 3600 + array[1] * 60 + array[2]
54
+ end
55
+ end
56
+
57
+ # The class that does the actual work.
58
+ class Ffmpeg
59
+ include Presets
60
+ include Utils
61
+
62
+ # The log file for ffmpeg. Progress data will be read from here.
63
+ LOG_FILE = 'ffmpeg.log'
64
+
65
+ # The extension for the default output file.
66
+ DEFAULT_EXT = 'mp4'
67
+
68
+ # The directory for the default output file.
69
+ DEFAULT_DIR = 'converted/'
70
+
71
+ # The default theme, change this with {#theme=}. {FfmpegProgress} expects
72
+ # a 256-color capable terminal.
73
+ DEFAULT_THEME = {
74
+ bars: 63, head: '[', full: '=', empty: '-', tail: ']',
75
+ end_fg: 202, full_fg: 214, empty_fg: 202,
76
+ time_fg: 214, block_fg: 214, finish_fg: 40, cancel_fg: 1
77
+ }
78
+
79
+ # The input file.
80
+ # @return [String]
81
+ attr_reader :input
82
+
83
+ # The output file. Directories will be auto-created on execution.
84
+ # @return [String]
85
+ attr_accessor :output
86
+
87
+ # Returns the duration (in seconds) of the input file.
88
+ # @return [Integer]
89
+ attr_reader :duration
90
+
91
+ # Returns the duration of the input file as a +ffmpeg+ format string
92
+ # (+HH:MM:SS.ms+).
93
+ #
94
+ # @return [Integer]
95
+ attr_reader :duration_ff
96
+
97
+ # The options to pass to +ffmpeg+. Any occurrences of the string +_INPUT_+
98
+ # will be converted to the value of the {#input} attribute.
99
+ # @return [String]
100
+ attr_accessor :options
101
+
102
+ # The theme for the progress bar. See {DEFAULT_THEME} for details.
103
+ # @return [Hash]
104
+ attr_accessor :theme
105
+
106
+ # The PID of the last spawned +ffmpeg+ process, or +nil+ if none spawned
107
+ # yet.
108
+ # @return [Integer]
109
+ attr_reader :pid
110
+
111
+ # Creates a new {Ffmpeg} instance.
112
+ #
113
+ # @param [String] input_file the source file name.
114
+ # @param [String] ffmpeg_options the options to pass to ffmpeg.
115
+ # See {#options} for details and {Presets} for examples.
116
+ # @return [Ffmpeg]
117
+ def initialize(input_file = nil, ffmpeg_options = DEFAULT_OPTS)
118
+ self.input = input_file
119
+ @options = ffmpeg_options
120
+
121
+ @output ||= nil
122
+ @duration_ff ||= nil
123
+ @duration ||= nil
124
+
125
+ @block = nil
126
+
127
+ @pid = nil
128
+ @theme = DEFAULT_THEME
129
+
130
+ @last_bar = nil
131
+ end
132
+
133
+ # Set the input file.
134
+ #
135
+ # @param [String] string
136
+ # @return [String]
137
+ def input=(string)
138
+ @input = string
139
+
140
+ return @input unless @input
141
+
142
+ @duration_ff = `ffmpeg -i \'#{@input}\' 2>&1`.scan(/..:..:..\.../).first
143
+ @duration = parse_ffmpeg_time(@duration_ff)
144
+
145
+ @output ||= "#{DEFAULT_DIR}#{@input.rpartition('.').first}.#{DEFAULT_EXT}"
146
+
147
+ @input
148
+ end
149
+
150
+ # Returns the current +ffmpeg+ command that will be executed by {#run}.
151
+ #
152
+ # @return [String]
153
+ def command
154
+ "ffmpeg -i \'#{@input}\' #{@options} \'#{@output}\' &> #{LOG_FILE}"
155
+ .gsub('_INPUT_', "'#{@input}'")
156
+ end
157
+
158
+ # Run +ffmpeg+ while printing a progress bar to the terminal.
159
+ # If a block is passed, its return value will be attached to the
160
+ # progress bar. The current {Ffmpeg} instance will be passed to
161
+ # the block.
162
+ #
163
+ # @return [0]
164
+ def run(&block)
165
+ File.delete(LOG_FILE) if File.exist?(LOG_FILE)
166
+
167
+ FileUtils.mkpath(@output.rpartition('/').first) if output.include?('/')
168
+
169
+ @pid = spawn command
170
+ @block = block
171
+
172
+ sleep 1 until current_time
173
+ monitor_progress
174
+
175
+ cleanup
176
+
177
+ 0
178
+ end
179
+
180
+ private
181
+
182
+ # Display the progress bar while waiting for +ffmpeg+ to finish.
183
+ def monitor_progress
184
+ until Process.waitpid(@pid, Process::WNOHANG)
185
+ print "\r#{progress_bar}"
186
+ sleep 1
187
+ end
188
+
189
+ puts "\r#{progress_bar(@theme[:finish_fg], @dur_string)}"
190
+ rescue Interrupt
191
+ puts "\r#{progress_bar(@theme[:cancel_fg])}"
192
+ end
193
+
194
+ # Display the progress bar.
195
+ def progress_bar(time_fg = @theme[:time_fg], time = current_time)
196
+ elements = bar_elements(time_fg, time)
197
+
198
+ bar =
199
+ "#{elements.head}#{elements.full}#{elements.empty}#{elements.tail}" \
200
+ "#{elements.head}#{elements.time}#{elements.tail}"
201
+
202
+ if @block
203
+ block_value = colorize(@block.call(self), @theme[:block_fg])
204
+ bar << "#{elements.head}#{block_value}#{elements.tail}"
205
+ end
206
+
207
+ bar
208
+ end
209
+
210
+ # Get the current time (in +ffmpeg+ format) from the log file.
211
+ # Returns +nil+ if the actual transcoding process has not started yet.
212
+ #
213
+ # @return [String]
214
+ def current_time
215
+ return nil unless File.exist?(LOG_FILE)
216
+ match = File.read(LOG_FILE).scan(/time=..:..:..\.../).last
217
+
218
+ return nil unless match
219
+ match.delete('time=')
220
+ end
221
+
222
+ # Generate bar elements for a given +ffmpeg+ time.
223
+ def bar_elements(time_fg, time)
224
+ elements = Struct.new(:head, :tail, :full, :empty, :time)
225
+ bars = (@theme[:bars] * parse_ffmpeg_time(time) / @duration).round
226
+
227
+ elements.new(
228
+ colorize(@theme[:head], @theme[:end_fg]),
229
+ colorize(@theme[:tail], @theme[:end_fg]),
230
+ colorize(@theme[:full] * bars, @theme[:full_fg]),
231
+ colorize(@theme[:empty] * (@theme[:bars] - bars), @theme[:empty_fg]),
232
+ colorize(time, time_fg)
233
+ )
234
+ end
235
+
236
+ # Kill +ffmpeg+ if still running, and delete the log file.
237
+ def cleanup
238
+ Process.detach(@pid)
239
+
240
+ ffmpeg_alive =
241
+ begin
242
+ Process.kill(0, @pid)
243
+ true
244
+ rescue Errno::ESRCH
245
+ false
246
+ end
247
+
248
+ Process.kill('TERM', @pid) if ffmpeg_alive
249
+
250
+ File.delete(LOG_FILE) if File.exist?(LOG_FILE)
251
+ end
252
+ end
253
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffmpeg_progress
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Winterbraid
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ Execute ffmpeg commands and display a progress bar.
15
+ email: Winterbraid@users.noreply.github.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.rdoc
22
+ - lib/ffmpeg_progress.rb
23
+ homepage: https://github.com/Winterbraid/ffmpeg_progress
24
+ licenses:
25
+ - MIT
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubyforge_project:
43
+ rubygems_version: 2.2.2
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: A fancy progress bar for ffmpeg.
47
+ test_files: []
48
+ has_rdoc: