Generate smart previews as .png so we don't suffer recompression losses when we convert the preview frame from a full size image down to a 150x150 .jpg thumbnail.
75 lines
1.8 KiB
Ruby
75 lines
1.8 KiB
Ruby
require "shellwords"
|
|
|
|
# A wrapper for the ffmpeg command.
|
|
class FFmpeg
|
|
extend Memoist
|
|
|
|
class Error < StandardError; end
|
|
|
|
attr_reader :file
|
|
|
|
# Operate on a file with FFmpeg.
|
|
# @param file [File, String] a webm, mp4, gif, or apng file
|
|
def initialize(file)
|
|
@file = file.is_a?(String) ? File.open(file) : file
|
|
end
|
|
|
|
# Generate a .png preview image for a video or animation. Generates
|
|
# thumbnails intelligently by avoiding blank frames.
|
|
#
|
|
# @return [MediaFile] the preview image
|
|
def smart_video_preview
|
|
vp = Tempfile.new(["video-preview", ".png"], binmode: true)
|
|
|
|
# https://ffmpeg.org/ffmpeg.html#Main-options
|
|
# https://ffmpeg.org/ffmpeg-filters.html#thumbnail
|
|
output = shell!("ffmpeg -i #{file.path.shellescape} -vf thumbnail=300 -frames:v 1 -y #{vp.path.shellescape}")
|
|
Rails.logger.debug(output)
|
|
|
|
MediaFile.open(vp)
|
|
end
|
|
|
|
# Get file metadata using ffprobe.
|
|
# @see https://ffmpeg.org/ffprobe.html
|
|
# @see https://gist.github.com/nrk/2286511
|
|
# @return [Hash] a hash of the file's metadata
|
|
def metadata
|
|
output = shell!("ffprobe -v quiet -print_format json -show_format -show_streams #{file.path.shellescape}")
|
|
json = JSON.parse(output)
|
|
json.with_indifferent_access
|
|
end
|
|
|
|
def width
|
|
video_channels.first[:width]
|
|
end
|
|
|
|
def height
|
|
video_channels.first[:height]
|
|
end
|
|
|
|
def duration
|
|
metadata.dig(:format, :duration).to_f
|
|
end
|
|
|
|
def video_channels
|
|
metadata[:streams].select { |stream| stream[:codec_type] == "video" }
|
|
end
|
|
|
|
def audio_channels
|
|
metadata[:streams].select { |stream| stream[:codec_type] == "audio" }
|
|
end
|
|
|
|
def has_audio?
|
|
audio_channels.present?
|
|
end
|
|
|
|
def shell!(command)
|
|
program = command.shellsplit.first
|
|
output, status = Open3.capture2e(command)
|
|
raise Error, "#{program} failed: #{output}" if !status.success?
|
|
output
|
|
end
|
|
|
|
memoize :metadata
|
|
end
|