mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-09 08:10:08 +09:00
9912e63d69
A video's preview and a picture's thumbnail are made inside the request that posts the message, and nothing bounded how long either could take. - The video preview filter also selects any frame from 5 seconds on. Rails' filter takes the second frame it selects, which a video with a single keyframe and no scene change only gives at its end, so ffmpeg decoded all of it. - TimeLimitedVideoPreviewer gives ffmpeg 10 seconds of wall-clock time, kills it past that, and reports a failed preview, so the message is posted without one. - Pictures and videos above 250 megapixels, or whose size couldn't be read, get no preview: decoding costs in proportion to the pixels, however small the file. - The view shows a preview only if it was made when the message was posted. Its URL used to make it on view, so a preview that failed or was skipped would be attempted again on every view. The cached presentation's version goes up, so cached messages pick this up. - A video's poster is made, when the message is posted, at the size the view shows it. The full-size WebP made until now wasn't shown anywhere, and encoding it costs in proportion to the frame's pixels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
60 lines
1.8 KiB
Ruby
60 lines
1.8 KiB
Ruby
module Message::Attachment
|
|
extend ActiveSupport::Concern
|
|
|
|
THUMBNAIL_MAX_WIDTH = 1200
|
|
THUMBNAIL_MAX_HEIGHT = 800
|
|
|
|
# Decoding a picture, or a video's frame, costs in proportion to its pixels, however small the file. The largest
|
|
# phone photos have 200 million.
|
|
THUMBNAIL_MAX_PIXELS = 250_000_000
|
|
|
|
included do
|
|
has_one_attached :attachment do |attachable|
|
|
attachable.variant :thumb, resize_to_limit: [ THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT ]
|
|
attachable.variant :poster, format: :webp, resize_to_limit: [ THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT ]
|
|
end
|
|
end
|
|
|
|
module ClassMethods
|
|
def create_with_attachment!(attributes)
|
|
create!(attributes).tap(&:process_attachment)
|
|
end
|
|
end
|
|
|
|
def attachment?
|
|
attachment.attached?
|
|
end
|
|
|
|
def process_attachment
|
|
ensure_attachment_analyzed
|
|
process_attachment_thumbnail
|
|
end
|
|
|
|
private
|
|
def ensure_attachment_analyzed
|
|
attachment&.analyze
|
|
end
|
|
|
|
# A file that ffmpeg or libvips can't decode is still the message: post it without a preview.
|
|
def process_attachment_thumbnail
|
|
return if too_many_pixels_to_preview?
|
|
|
|
case
|
|
when attachment.video?
|
|
attachment.preview(:poster).processed
|
|
when attachment.representable?
|
|
attachment.representation(:thumb).processed
|
|
end
|
|
rescue ActiveStorage::PreviewError, Vips::Error => error
|
|
Rails.logger.warn "Posted #{attachment.filename} without a preview: #{error.class}: #{error.message.lines.first&.chomp}"
|
|
end
|
|
|
|
# Without a size, the analyzer couldn't read the file's header, and the previewer wouldn't either.
|
|
def too_many_pixels_to_preview?
|
|
if attachment.image? || attachment.video?
|
|
width, height = attachment.metadata.values_at(:width, :height)
|
|
width.nil? || height.nil? || width * height > THUMBNAIL_MAX_PIXELS
|
|
end
|
|
end
|
|
end
|