Class: ActiveStorage::HotCell::Server::Analyzers::Media::Ffprobe

Inherits:
ToolOperation show all
Defined in:
lib/active_storage/hot_cell/server/analyzers/media/ffprobe.rb

Overview

What Rails' video and audio analyzers do, moved out of the application: ffprobe, and the numbers it reports shaped exactly as Analyzer::VideoAnalyzer and Analyzer::AudioAnalyzer shape them. One operation serves both client analyzers, because ffprobe reports video and audio streams in one pass.

This reads its tool's stdout, and that is a judgement, so here it is.

ffprobe parses the media in an exec'd child that dies at the end of the call. What comes back into this worker is JSON on a bounded buffer, parsed by JSON.parse — not a media decoder, and not a parser with a history of memory-safety bugs. The question that decides whether recycling a worker is safe is whether a malicious input can execute in this process, and through ffprobe's JSON it cannot. Reading a tool's output file with an in-process media library is in-process decoding; parsing its structured stdout with the standard library is not.

The result is a superset of what either analyzer writes, and the client analyzers slice it to Rails' exact keys — the same split the image analyzer uses. Two things here are deliberately not Rails. Every number is coerced tolerantly rather than with Integer()/Float() that raise, because ffprobe's output is attacker-controlled where Rails' is trusted; a value that is not a clean number is dropped rather than crashing the analysis. And tags — title, artist, arbitrary bytes that need not be valid UTF-8 — are never returned, where Rails writes them straight into the database.

Constant Summary collapse

ROTATIONS =
[ 90, 270, -90, -270 ].freeze

Constants inherited from Operation

Operation::CONTENT_TYPES

Instance Method Summary collapse

Instance Method Details

#perform(inputs, _outputs, probe_arguments: []) ⇒ Object

probe_arguments is config.active_storage.ffprobe_arguments, split, and it goes where Rails puts it: before the input path, which is where an input option such as -codec_whitelist has to be to take effect.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/active_storage/hot_cell/server/analyzers/media/ffprobe.rb', line 40

def perform(inputs, _outputs, probe_arguments: [])
  source, = inputs
  probed = JSON.parse(run!("ffprobe", "-v", "quiet", "-print_format", "json",
                           "-show_format", "-show_streams",
                           *arguments!(:probe_arguments, probe_arguments), source.fd_path,
                           pass: [ source.to_io ]).out)

  video = stream_of(probed, "video")
  audio = stream_of(probed, "audio")

  { duration: duration(probed, video, audio), bytes: source.to_io.stat.size,
    video: !video.nil?, audio: !audio.nil? }
    .merge(video ? (video) : {})
    .merge(audio ? (audio) : {})
    .compact
rescue JSON::ParserError => error
  raise UnreadableDocument, "ffprobe said something that is not JSON: #{error.message[0, 120]}"
end