module Recordings class GenerateThumbnail class Error < StandardError; end def initialize(recording, video_path: nil) @recording = recording @video_path = video_path end def call return @recording if @recording.thumbnail_storage_key.present? path = @video_path || local_video_path return @recording unless path && File.exist?(path) thumb_path = extract_frame(path) key = thumbnail_key Recordings::Storage.new.upload(local_path: thumb_path, key: key, content_type: "image/jpeg") @recording.update!(thumbnail_storage_key: key) FileUtils.rm_f(thumb_path) @recording rescue Recordings::Storage::Error => e Rails.logger.warn("[Recordings::GenerateThumbnail] #{@recording.id}: #{e.message}") @recording ensure FileUtils.rm_f(@thumb_temp) if @thumb_temp && File.exist?(@thumb_temp) end private def local_video_path return nil if @recording.storage_key.blank? Recordings::Storage.new.local_path_for_key(@recording.storage_key) end def extract_frame(video_path) @thumb_temp = File.join(Dir.tmpdir, "thumb-#{@recording.id}-#{SecureRandom.hex(4)}.jpg") dur = @recording.duration_secs.to_i offset = dur.positive? ? [[dur / 4, 5].max, dur - 1].min : 1 success = system( "ffmpeg", "-y", "-ss", offset.to_s, "-i", video_path, "-vframes", "1", "-q:v", "2", @thumb_temp, out: File::NULL, err: File::NULL ) raise Error, "ffmpeg thumbnail failed" unless success && File.exist?(@thumb_temp) @thumb_temp end def thumbnail_key "teams/#{@recording.team_id}/sessions/#{@recording.stream_session_id}/thumbnail.jpg" end end end