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