class ActiveStorage::Analyzer::VideoAnalyzer

This analyzer requires the FFmpeg system library, which is not provided by Rails.
When a video’s angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
# => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
Example:
* Video (true if file has an video channel, false if not)
* Audio (true if file has an audio channel, false if not)
* Display aspect ratio
* Angle (degrees)
* Duration (seconds)
* Height (pixels)
* Width (pixels)
Extracts the following from a video blob:

def self.accept?(blob)

def self.accept?(blob)
  blob.video?
end

def angle

def angle
  Integer(tags["rotate"]) if tags["rotate"]
end

def audio?

def audio?
  audio_stream.present?
end

def audio_stream

def audio_stream
  @audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
end

def computed_height

def computed_height
  if encoded_width && display_height_scale
    encoded_width * display_height_scale
  end
end

def container

def container
  probe["format"] || {}
end

def display_aspect_ratio

def display_aspect_ratio
  if descriptor = video_stream["display_aspect_ratio"]
    if terms = descriptor.split(":", 2)
      numerator   = Integer(terms[0])
      denominator = Integer(terms[1])
      [numerator, denominator] unless numerator == 0
    end
  end
end

def display_height_scale

def display_height_scale
  @display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
end

def duration

def duration
  duration = video_stream["duration"] || container["duration"]
  Float(duration) if duration
end

def encoded_height

def encoded_height
  @encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
end

def encoded_width

def encoded_width
  @encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
end

def ffprobe_path

def ffprobe_path
  ActiveStorage.paths[:ffprobe] || "ffprobe"
end

def height

def height
  if rotated?
    encoded_width
  else
    computed_height || encoded_height
  end
end

def metadata

def metadata
  { width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio?, video: video? }.compact
end

def probe

def probe
  @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
end

def probe_from(file)

def probe_from(file)
  instrument(File.basename(ffprobe_path)) do
    IO.popen([ ffprobe_path,
      "-print_format", "json",
      "-show_streams",
      "-show_format",
      "-v", "error",
      file.path
    ]) do |output|
      JSON.parse(output.read)
    end
  end
rescue Errno::ENOENT
  logger.info "Skipping video analysis because ffprobe isn't installed"
  {}
end

def rotated?

def rotated?
  angle == 90 || angle == 270
end

def streams

def streams
  probe["streams"] || []
end

def tags

def tags
  @tags ||= video_stream["tags"] || {}
end

def video?

def video?
  video_stream.present?
end

def video_stream

def video_stream
  @video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
end

def width

def width
  if rotated?
    computed_height || encoded_height
  else
    encoded_width
  end
end