Class: MediaTrim

Inherits:
Object
  • Object
show all
Defined in:
lib/trim_run.rb,
lib/trim_help.rb,
lib/trim_main.rb,
lib/trim_class.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename = nil, trimmed_filename = nil, start = '0', to = nil, **options) ⇒ MediaTrim

Returns a new instance of MediaTrim.

Parameters:

  • to (String) (defaults to: nil)

    end timecode; duration not supported for API



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, **options)
  @fname = MediaTrim.expand_env(filename) if filename
  @copy_filename = MediaTrim.expand_env(trimmed_filename) if trimmed_filename
  @start = MediaTrim.time_format start
  @interval = ['-ss', MediaTrim.time_format(@start)]

  @overwrite = options[:overwrite] ? '-y' : '-n'
  @quiet     = options[:quiet].nil? || options[:quiet] ? ['-hide_banner', '-loglevel', 'error', '-nostats'] : []
  @view      = options[:view].nil? ? true : options[:view]

  prepare(@start, to, mode: :timecode) if to
end

Instance Attribute Details

#copy_filenameObject

Returns the value of attribute copy_filename.



4
5
6
# File 'lib/trim_main.rb', line 4

def copy_filename
  @copy_filename
end

#fnameObject

Returns the value of attribute fname.



4
5
6
# File 'lib/trim_main.rb', line 4

def fname
  @fname
end

#intervalObject

Returns the value of attribute interval.



4
5
6
# File 'lib/trim_main.rb', line 4

def interval
  @interval
end

#msg_endObject

Returns the value of attribute msg_end.



4
5
6
# File 'lib/trim_main.rb', line 4

def msg_end
  @msg_end
end

#overwriteObject

Returns the value of attribute overwrite.



4
5
6
# File 'lib/trim_main.rb', line 4

def overwrite
  @overwrite
end

#quietObject

Returns the value of attribute quiet.



4
5
6
# File 'lib/trim_main.rb', line 4

def quiet
  @quiet
end

#startObject

Returns the value of attribute start.



4
5
6
# File 'lib/trim_main.rb', line 4

def start
  @start
end

#viewObject

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.

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

#optionsObject



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 options
  OptionParser.new do |opts|
    opts.banner = "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

#runObject

Raises:



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

Parameters:

  • argv (array)

    Copy of ARGV

Raises:



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