|
# frozen_string_literal: true |
|
|
|
# Run: ruby optimize-video.rb --help |
|
# For AMD GPUs, you need to build ffmpeg with AMF support |
|
# Only optimizes .mp4 and .mov files, but can easily be adjusted in collect_videos() function |
|
|
|
require 'bundler/inline' |
|
|
|
gemfile do |
|
source 'https://rubygems.org' |
|
gem 'streamio-ffmpeg', '~> 3.0', '>= 3.0.2' |
|
gem 'concurrent-ruby', '~> 1.3', '>= 1.3.3' |
|
gem 'tty-progressbar', '~> 0.18.2' |
|
gem 'optparse', '~> 0.5.0' |
|
gem 'logger', '~> 1.6' |
|
gem 'fileutils', '~> 1.7', '>= 1.7.2' |
|
end |
|
|
|
PREFIX = 'optimized' |
|
@converted_folder = 'converted' |
|
|
|
def collect_videos |
|
Dir.glob('*.*') |
|
.select { |file| FFMPEG::Movie.new(file).valid? } |
|
.reject { |file| file.start_with?(PREFIX) } |
|
.select { |file| file.match?(/mp4|mov/) } |
|
end |
|
|
|
def init_thread_pool(threads) |
|
Concurrent::FixedThreadPool.new( |
|
threads, |
|
idletime: 10, |
|
name: 'optimizer', |
|
fallback_policy: :discard, |
|
auto_terminate: true |
|
) |
|
end |
|
|
|
def queue_and_transcode(video, queue_id, argv) |
|
@thread_pool.post do |
|
@logger.info("#{queue_id}: Processing #{video}") |
|
|
|
bar = @bars.register( |
|
"Thread (#{queue_id} of #{@total_jobs}): [:bar] :percent ETA :eta", |
|
bar_format: :block, |
|
total: 10 |
|
) |
|
|
|
movie = FFMPEG::Movie.new(video) |
|
creation_meta_data = movie.metadata[:streams][0][:tags][:creation_time] |
|
creation_meta_data = movie.metadata[:format][:tags][:creation_time] if creation_meta_data.nil? |
|
@logger.warn("#{queue_id}: #{movie} metadata creation_time had value nil") if creation_meta_data.nil? |
|
|
|
creation_time = ['-metadata', "creation_time=#{creation_meta_data}"] |
|
|
|
options = { |
|
custom: [ |
|
'-map_metadata', '0', |
|
'-movflags', 'use_metadata_tags', |
|
*creation_time, |
|
'-c:a', 'copy', |
|
'-b:v', '1M', |
|
'-maxrate', '2M', |
|
'-bufsize', '1M', |
|
'-vf', 'scale=-2:720', |
|
'-b:a', '128k' |
|
] |
|
} |
|
|
|
options[:custom] += ['-c:v', 'h264_nvenc'] if argv[:enable_gpu_nvidia] |
|
options[:custom] += ['-c:v', 'h264_amf'] if argv[:enable_gpu_amd] |
|
|
|
options[:threads] = argv[:ffmpeg_threads] if argv[:ffmpeg_threads] |
|
|
|
new_file_format = argv[:output] ? ".#{argv[:output]}" : File.extname(video) |
|
new_file = "#{PREFIX}/#{PREFIX}-#{File.basename(video, '.*')}#{new_file_format}" |
|
|
|
movie.transcode(new_file, options) { |progress| bar.current = progress * 10 } |
|
|
|
# Preserve file system timestamps |
|
stat = File.stat(video) |
|
File.utime(stat.atime, stat.mtime, new_file) |
|
|
|
FileUtils.move video, "#{@converted_folder}/#{video}" |
|
|
|
bar.finish |
|
@completed_jobs += 1 |
|
@logger.info("#{queue_id}: Completed processing #{video}") |
|
rescue StandardError => e |
|
@logger.error("Error processing #{video}: #{e.message}") |
|
@logger.error(e.backtrace.join("\n")) |
|
end |
|
end |
|
|
|
argv = {} |
|
gpu_enabled = false |
|
opt = OptionParser.new do |option| |
|
option.on('--enable-gpu-nvidia', 'Enable GPU acceleration (NVIDIA card)') do |
|
argv[:enable_gpu_nvidia] = true |
|
gpu_enabled = true |
|
end |
|
|
|
option.on('--enable-gpu-amd', 'Enable GPU acceleration (AMD card)') do |
|
argv[:enable_gpu_amd] = true |
|
gpu_enabled = true |
|
end |
|
|
|
option.on('--threads=NUMBER', 'Set the number of worker threads') do |amount| |
|
argv[:worker_threads] = amount.to_i |
|
end |
|
|
|
option.on('--ffmpeg-threads=NUMBER', 'Set the number of threads each ffmpeg process should use') do |amount| |
|
argv[:ffmpeg_threads] = amount.to_i |
|
end |
|
|
|
option.on('--output-format=STRING', 'Sets the preferred output format') do |out_format| |
|
argv[:output] = out_format |
|
end |
|
|
|
option.on('-h', '--help', 'See available options') do |
|
puts option |
|
return 0 |
|
end |
|
end |
|
opt.parse! |
|
|
|
Dir.mkdir(@converted_folder) unless Dir.exist?(@converted_folder) |
|
Dir.mkdir(PREFIX) unless Dir.exist?(PREFIX) |
|
|
|
FFMPEG.logger = @logger = Logger.new('transcode_log.txt') |
|
WORKER_THREADS = argv[:worker_threads].nil? ? Concurrent.processor_count : argv[:worker_threads] |
|
@thread_pool = init_thread_pool(WORKER_THREADS) |
|
|
|
@bars = TTY::ProgressBar::Multi.new( |
|
"Main (running #{WORKER_THREADS} #{WORKER_THREADS > 1 ? 'threads' : 'thread'}) #{if gpu_enabled |
|
'with GPU acceleration' |
|
end}", |
|
frequency: 1.0 |
|
) |
|
@bars.start |
|
|
|
videos = collect_videos |
|
@completed_jobs = 0 |
|
@total_jobs = videos.length |
|
|
|
videos.each_with_index do |video, index| |
|
@logger.info("Queueing video: #{video}") |
|
queue_and_transcode(video, index + 1, argv) |
|
end |
|
|
|
# Only shutdown thread_pool when all jobs completed, until then, sleep and check later |
|
@logger.info('Waiting for all jobs to complete') |
|
while @completed_jobs < @total_jobs |
|
sleep 30 |
|
@logger.info("Completed #{@completed_jobs}/#{@total_jobs} jobs") |
|
end |
|
@logger.info('All jobs completed') |
|
|
|
@logger.info('Thread pool shutdown') |
|
@thread_pool.shutdown |
|
@logger.info('Thread pool wait_for_termination') |
|
@thread_pool.wait_for_termination |
|
@logger.info('Thread pool terminated') |
|
|
|
@bars.finish |
|
|
|
puts |
|
print 'Press any key to close' |
|
gets |