114 lines
3.2 KiB
Ruby
114 lines
3.2 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
class LoadedMedia::WebmOrMp4 < LoadedMedia
|
|
include HasColorLogger
|
|
|
|
::FFMPEG.logger = Logger.new(nil)
|
|
FFMPEG_BIN = T.let(`which ffmpeg`.strip, String)
|
|
FFPROBE_BIN = T.let(`which ffprobe`.strip, String)
|
|
|
|
sig { params(media_path: String).void }
|
|
def initialize(media_path)
|
|
@media_path = media_path
|
|
@media = T.let(FFMPEG::Media.new(media_path), FFMPEG::Media)
|
|
duration = @media.duration
|
|
# frame_rate = @media.frame_rate
|
|
# @num_frames = T.let((frame_rate * duration).to_i, Integer)
|
|
|
|
output, error, status =
|
|
Open3.capture3(
|
|
FFPROBE_BIN,
|
|
"-v",
|
|
"error",
|
|
"-count_frames",
|
|
"-select_streams",
|
|
"v:0",
|
|
"-show_entries",
|
|
"stream=nb_read_frames",
|
|
"-of",
|
|
"default=nokey=1:noprint_wrappers=1",
|
|
media_path,
|
|
)
|
|
|
|
if status.success?
|
|
@num_frames = T.let(output.strip.to_i, Integer)
|
|
else
|
|
$stderr.print error
|
|
raise "Failed to get frame count from ffprobe: exit code #{status.exitstatus}"
|
|
end
|
|
|
|
@duration = T.let(duration, Float)
|
|
raise("no frames found in webm") if @num_frames.zero?
|
|
end
|
|
|
|
sig { override.returns(Integer) }
|
|
def num_frames
|
|
@num_frames
|
|
end
|
|
|
|
sig do
|
|
override
|
|
.params(frame: Integer, path: String, options: ThumbnailOptions)
|
|
.void
|
|
end
|
|
def write_frame_thumbnail(frame, path, options)
|
|
# Clamp the frame number to valid range for calculation
|
|
clamped_frame = frame.clamp(0, @num_frames - 1)
|
|
|
|
# Calculate frame time using the clamped frame number
|
|
frame_time = (clamped_frame.to_f * @duration) / @num_frames
|
|
frame_time = frame_time.clamp(0.0..(@duration * 0.98))
|
|
|
|
logger.info(
|
|
format_tags(
|
|
make_tag("frame", frame),
|
|
make_tag("frame_time", frame_time),
|
|
make_tag("duration", @duration),
|
|
make_tag("num_frames", @num_frames),
|
|
),
|
|
)
|
|
|
|
# Use the original frame number in the filename
|
|
tmp_path =
|
|
File.join(BlobFile::TMP_DIR, "webm-#{frame}-#{SecureRandom.uuid}.png")
|
|
FileUtils.mkdir_p(File.dirname(tmp_path))
|
|
|
|
# Always seek from the beginning for simplicity and reliability
|
|
# Add a small safety margin for the last frame to avoid seeking beyond video duration
|
|
safe_seek_time = [frame_time, @duration - 0.01].min
|
|
|
|
cmd = [
|
|
FFMPEG_BIN,
|
|
"-y", # Overwrite output files
|
|
"-xerror", # Exit on error
|
|
"-abort_on",
|
|
"empty_output", # Abort if output is empty
|
|
"-ss",
|
|
safe_seek_time.round(2).to_s,
|
|
"-i",
|
|
@media_path, # Input file
|
|
"-vframes",
|
|
"1", # Extract one frame
|
|
"-f",
|
|
"image2", # Force format to image2
|
|
"-update",
|
|
"1", # Update existing file
|
|
tmp_path,
|
|
]
|
|
|
|
_output, error, status = Open3.capture3(*cmd)
|
|
unless status.exitstatus == 0
|
|
$stderr.print error
|
|
raise "Failed to extract frame with ffmpeg: #{cmd.join(" ")}: #{error}"
|
|
end
|
|
|
|
# Use the original frame number in the error message
|
|
raise("screenshot @ #{clamped_frame} failed") unless File.exist?(tmp_path)
|
|
image = Vips::Image.new_from_file(tmp_path)
|
|
write_image_thumbnail(image, path, options)
|
|
ensure
|
|
FileUtils.rm_rf(tmp_path) if tmp_path && File.exist?(tmp_path)
|
|
end
|
|
end
|