Class: MediaTrim
- Inherits:
-
Object
- Object
- MediaTrim
- Defined in:
- lib/trim_run.rb,
lib/trim_help.rb,
lib/trim_main.rb,
lib/trim_class.rb
Instance Attribute Summary collapse
-
#copy_filename ⇒ Object
Returns the value of attribute copy_filename.
-
#fname ⇒ Object
Returns the value of attribute fname.
-
#interval ⇒ Object
Returns the value of attribute interval.
-
#msg_end ⇒ Object
Returns the value of attribute msg_end.
-
#overwrite ⇒ Object
Returns the value of attribute overwrite.
-
#quiet ⇒ Object
Returns the value of attribute quiet.
-
#start ⇒ Object
Returns the value of attribute start.
-
#view ⇒ Object
Returns the value of attribute view.
Class Method Summary collapse
- .add_times(str1, str2) ⇒ Object
-
.duration(str1, str2) ⇒ Object
Time difference HH:MM:SS, ignoring millis.
-
.expand_env(str, die_if_undefined: false) ⇒ Object
Expand an environment variable reference.
- .help(msg = nil) ⇒ Object
- .mk_time(str) ⇒ Object
- .time_format(elapsed_seconds) ⇒ Object
- .to_seconds(str) ⇒ Object
Instance Method Summary collapse
-
#initialize(filename = nil, trimmed_filename = nil, start = '0', to = nil, **options) ⇒ MediaTrim
constructor
A new instance of MediaTrim.
- #options ⇒ Object
- #prepare(from, duration_or_timecode, mode: :duration) ⇒ Object
- #run ⇒ Object
- #setup(argv) ⇒ Object
Constructor Details
#initialize(filename = nil, trimmed_filename = nil, start = '0', to = nil, **options) ⇒ MediaTrim
Returns a new instance of MediaTrim.
7 8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/trim_main.rb', line 7 def initialize(filename = nil, trimmed_filename = nil, start = '0', to = nil, **) @fname = MediaTrim.(filename) if filename @copy_filename = MediaTrim.(trimmed_filename) if trimmed_filename @start = MediaTrim.time_format start @interval = ['-ss', MediaTrim.time_format(@start)] @overwrite = [:overwrite] ? '-y' : '-n' @quiet = [:quiet].nil? || [:quiet] ? ['-hide_banner', '-loglevel', 'error', '-nostats'] : [] @view = [:view].nil? ? true : [:view] prepare(@start, to, mode: :timecode) if to end |
Instance Attribute Details
#copy_filename ⇒ Object
Returns the value of attribute copy_filename.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def copy_filename @copy_filename end |
#fname ⇒ Object
Returns the value of attribute fname.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def fname @fname end |
#interval ⇒ Object
Returns the value of attribute interval.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def interval @interval end |
#msg_end ⇒ Object
Returns the value of attribute msg_end.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def msg_end @msg_end end |
#overwrite ⇒ Object
Returns the value of attribute overwrite.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def overwrite @overwrite end |
#quiet ⇒ Object
Returns the value of attribute quiet.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def quiet @quiet end |
#start ⇒ Object
Returns the value of attribute start.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def start @start end |
#view ⇒ Object
Returns the value of attribute view.
4 5 6 |
# File 'lib/trim_main.rb', line 4 def view @view end |
Class Method Details
.add_times(str1, str2) ⇒ Object
2 3 4 5 6 7 8 9 10 11 12 13 |
# File 'lib/trim_class.rb', line 2 def self.add_times(str1, str2) time1 = Time.parse mk_time str1 time2 = Time.parse mk_time str2 h = time2.strftime('%H').to_i m = time2.strftime('%M').to_i s = time2.strftime('%S').to_i millis = time2.strftime('%L').to_f / 1000.0 sum = (time1 + (h * 60 * 60) + (m * 60) + s + millis) return sum.strftime('%H:%M:%S') if h.positive? sum.strftime('%M:%S') end |
.duration(str1, str2) ⇒ Object
Returns time difference HH:MM:SS, ignoring millis.
27 28 29 30 31 32 |
# File 'lib/trim_class.rb', line 27 def self.duration(str1, str2) time1 = Time.parse mk_time str1 time2 = Time.parse mk_time str2 MediaTrim.time_format(time2 - time1) end |
.expand_env(str, die_if_undefined: false) ⇒ Object
Expand an environment variable reference
35 36 37 38 39 40 41 42 43 |
# File 'lib/trim_class.rb', line 35 def self.(str, die_if_undefined: false) str&.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do envar = Regexp.last_match(1) raise TrimError, "MediaTrim error: #{envar} is undefined".red, [] \ if !ENV.key?(envar) && die_if_undefined # Suppress stack trace ENV.fetch(envar, nil) end end |
.help(msg = nil) ⇒ Object
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 |
# File 'lib/trim_help.rb', line 2 def self.help(msg = nil) puts "Error: #{msg}.\n".red if msg puts <<~END_HELP media_trim - Trim an audio or video file using ffmpeg - Works with all formats supported by ffmpeg, including mp3, mp4, mkv, and many more. - Seeks to the nearest frame positions by re-encoding the media. - Reduces file size produced by OBS Studio by over 80 percent. - Can be used as a Ruby gem. - Installs the 'trim' command. When run as a command, output files are named by adding a 'trim.' prefix to the media file name, e.g. 'dir/trim.file.ext'. By default, the trim command does not overwrite pre-existing output files. When trimming is complete, the trim command displays the trimmed file, unless the -q option is specified Command-line Usage: trim [OPTIONS] dir/file.ext start [[to|for] end] - The start and end timecodes have the format [HH:[MM:]]SS[.XXX] Note that decimal seconds may be specified, but frames may not; this is consistent with how ffmpeg parses timecodes. - end defaults to the end of the audio/video file OPTIONS are: -d Enable debug output -f Overwrite output file if present -h Display help information. -v Verbose output -V Do not view the trimmed file when complete. Examples: # Crop dir/file.mp4 from 15.0 seconds to the end of the video, save to demo/trim.demo.mp4: trim demo/demo.mp4 15 # Crop dir/file.mkv from 3 minutes, 25 seconds to 9 minutes, 35 seconds, save to demo/trim.demo.mp4: trim demo/demo.mp4 3:25 9:35 # Same as the previous example, using optional 'to' syntax: trim demo/demo.mp4 3:25 to 9:35 # Save as the previous example, but specify the duration instead of the end time by using the for keyword: trim demo/demo.mp4 3:25 for 6:10 Need a way to figure out the start and stop times to trim a video? DJV is an excellent video viewer https://darbyjohnston.github.io/DJV/ - allows frame-by-frame stepping - displays the current time reliabily - F/OSS - Mac, Windows, Linux - High quality END_HELP exit 1 end |
.mk_time(str) ⇒ Object
45 46 47 48 49 50 51 52 |
# File 'lib/trim_class.rb', line 45 def self.mk_time(str) case str.count ':' when 0 then "0:0:#{str}" when 1 then "0:#{str}" when 2 then str else raise TrimError, "Error: #{str} is not a valid time" end end |
.time_format(elapsed_seconds) ⇒ Object
15 16 17 18 19 20 21 22 23 24 |
# File 'lib/trim_class.rb', line 15 def self.time_format(elapsed_seconds) elapsed_time = elapsed_seconds.to_i hours = (elapsed_time / (60 * 60)).to_i minutes = ((elapsed_time - (hours * 60)) / 60).to_i seconds = elapsed_time - (hours * 60 * 60) - (minutes * 60) result = "#{minutes.to_s.rjust 2, '0'}:#{seconds.to_s.delete_suffix('.0').rjust 2, '0'}" result = "#{hours}:#{result}}" unless hours.zero? result end |
.to_seconds(str) ⇒ Object
54 55 56 57 58 59 60 61 62 |
# File 'lib/trim_class.rb', line 54 def self.to_seconds(str) array = str.split(':').map(&:to_i).reverse case array.length when 1 then str.to_i when 2 then array[0] + (array[1] * 60) when 3 then array[0] + (array[1] * 60) + (array[2] * 60 * 60) else raise TrimError, "Error: #{str} is not a valid time" end end |
Instance Method Details
#options ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/trim_main.rb', line 20 def OptionParser.new do |opts| opts. = "Usage: #{$PROGRAM_NAME} [options]" opts.on('-f', '--[no-]@overwrite', 'Overwrite any previous output') do |f| @overwrite = f ? '-y' : '-n' end opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v| @quiet = [] if v end opts.on('-h', '', 'Display help') do |_| help end opts.on('-V', '--[no-]@view', 'View ffmpeg output') do |v| @view = false if v end end.parse! end |
#prepare(from, duration_or_timecode, mode: :duration) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/trim_main.rb', line 73 def prepare(from, duration_or_timecode, mode: :duration) if mode == :duration timecode = MediaTrim.time_format duration_or_timecode time_end = MediaTrim.add_times from, timecode @interval += ['-t', time_end] @msg_end = " for a duration of #{timecode} (until #{time_end})" else # end timecode was specified time_end = MediaTrim.time_format(MediaTrim.to_seconds(duration_or_timecode)) elapsed_time = MediaTrim.duration from, time_end @interval += ['-to', time_end] @msg_end = " to #{time_end} (duration #{elapsed_time})" end time_end end |
#run ⇒ Object
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 |
# File 'lib/trim_run.rb', line 2 def run raise TrimError, 'Error: No filename was specified'.red unless @fname raise TrimError, 'Error: No trimmed filename was specified'.red unless @copy_filename raise TrimError, 'Error: No starting timestamp was specified'.red unless @start raise TrimError, 'Error: Starting timestamp must be a string'.red unless @start.instance_of? String puts "Trimming '#{@fname}' from #{@start}#{@msg_end}".cyan command = ['ffmpeg', *@quiet, '-hwaccel', 'auto', @overwrite, '-i', @fname, '-acodec', 'aac', *@interval, @copy_filename] # puts command.join(' ').yellow start_clock = Process.clock_gettime(Process::CLOCK_MONOTONIC) status = system(*command) end_clock = Process.clock_gettime(Process::CLOCK_MONOTONIC) elapsed = end_clock - start_clock puts "Trim took #{MediaTrim.time_format elapsed.to_i}".cyan $stdout.flush exit 1 unless status # View trimmed file unless -q option was specified return unless @view # Open in Windows if running in WSL if File.exist? '/mnt/c/Program Files/DJV2/bin/djv.com' realpath = File.realpath @copy_filename windows_path = `wslpath -m '#{realpath}'`.chomp spawn 'cmd.exe', '/c', 'C:\\Program Files\\DJV2\\bin\\djv.com', '-full_screen', '-full_screen_monitor', '2', windows_path elsif `which cmd.exe` exec 'cmd.exe', '/C', '@start', @copy_filename, "--extraintf='luaintf{intf=\"looper_custom_time\"}'" elsif `which xdg-open` # Open any file with its default Linux application with xdg-open. # Define default apps in ~/.local/share/applications/defaults.list, # which is read on every invocation. # See https://askubuntu.com/questions/809981/set-the-default-video-player-from-the-command-line exec 'xdg-open', @copy_filename end end |
#setup(argv) ⇒ Object
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 |
# File 'lib/trim_main.rb', line 40 def setup(argv) MediaTrim.help 'Please specify the name of the video file to trim' unless argv[0] @fname = MediaTrim. argv[0] unless File.exist? @fname puts "Error: '#{File.realpath @fname}' does not exist.".red exit 1 end original_filename = File.basename @fname, '.*' ext = File.extname @fname @copy_filename = "#{File.dirname @fname}/trim.#{original_filename}#{ext}" MediaTrim.help 'Please specify the time to @start trimming the video file from' unless argv[1] @start = MediaTrim.time_format argv[1] @interval = ['-ss', @start] @msg_end = '' index = 2 return unless argv.length > index if argv[index] == 'for' # duration index += 1 MediaTrim.help 'No duration was specified' unless argv.length > index to = prepare @start, argv[index], mode: :duration else # end timecode index += 1 if argv[index] == 'to' MediaTrim.help 'No end time was specified' unless argv.length > index to = prepare @start, argv[index], mode: :timecode end return unless @start >= to raise TrimError, "Error: @start time (#{@start}) must be before end time (#{to})" if @start >= to end |