Class: ActiveStorage::HotCell::Server::Previewers::Video::Ffmpeg
- Inherits:
-
ToolOperation
- Object
- HotCell::Operation
- Operation
- ToolOperation
- ActiveStorage::HotCell::Server::Previewers::Video::Ffmpeg
- Defined in:
- lib/active_storage/hot_cell/server/previewers/video/ffmpeg.rb
Overview
What ActiveStorage::Previewer::VideoPreviewer does, moved out of the application.
Rails runs ffmpeg <input args> -i <file> <output args> - and lets an application set both
argument lists as shell strings it splits with Shellwords. This honours the input half:
video_preview_input_arguments arrives split, and it goes before -i exactly as it does in
Rails, because that is where an input option has to be. The output half is fixed here — see
RELEVANT_FRAME — and the only other thing a caller says is how many seconds in to seek.
Constant Summary collapse
- MAX_SEEK =
86_400- RELEVANT_FRAME =
Rails' own frame selection, verbatim from the 7.0 defaults: frame 0, keyframes, and scene changes over 0.015 are selected, and the loop/trim pair yields the second of those with the first as the fallback — so a video that opens on black previews as its first scene rather than as the black. This is the output half of
video_preview_arguments, and it stays fixed: the input half is what an application needs to shape, and that travels separately asinput_arguments. 'select=eq(n\,0)+eq(key\,1)+gt(scene\,0.015),loop=loop=-1:size=2,trim=start_frame=1'
Constants inherited from Operation
Instance Method Summary collapse
Instance Method Details
#perform(inputs, outputs, seek: 0, input_arguments: []) ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/active_storage/hot_cell/server/previewers/video/ffmpeg.rb', line 34 def perform(inputs, outputs, seek: 0, input_arguments: []) source, = inputs destination, = outputs seek = seek!(seek) input_arguments = arguments!(:input_arguments, input_arguments) # JPEG rather than PNG, because Rails' own video previewer yields image/jpeg and this is meant to # drop into its place. A preview that changed the attached blob's content type would not be a # replacement. # # -ss before -i seeks by keyframe, which is orders of magnitude cheaper on a long file than # decoding up to the point. -nostdin because ffmpeg reads the terminal otherwise, and a worker has # no terminal. The application's input arguments go directly before -i, so they cannot drift # past it as the fixed flags around them change. # # Both descriptors go to ffmpeg: it reads /dev/fd for the source and writes /dev/fd for the frame, # so neither the input nor the output touches scratch. Writing the descriptor directly leaves # partial bytes in the caller's file on a mid-write failure where a staged output would have left # it empty — harmless here, because run! turns a non-zero exit into a refusal and the previewer # discards its output on any failure, but it is a real property of the direct write. run! "ffmpeg", "-nostdin", "-loglevel", "error", "-ss", seek.to_s, *input_arguments, "-i", source.fd_path, "-vf", RELEVANT_FRAME, "-frames:v", "1", "-f", "image2", "-c:v", "mjpeg", "-y", destination.fd_path, pass: [ source.to_io, destination.to_io ] { format: "jpg", content_type: "image/jpeg", seek: seek, bytes: produced!(destination.to_io.stat.size, "ffmpeg") } end |