Files
MatchLiveTv/backend/app/services/streams/generate_cover_slate.rb
eminuxandCursor 41d4235258 Aggiunge copertina sponsor custom solo Premium Full, con override in wizard e slate sui nodi di streaming.
Permette upload da portale e app con ereditarietà partita→squadra→società, generazione MP4 via ffmpeg e distribuzione su home lab e nodi CPX per pause e assenza segnale.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 23:47:17 +02:00

122 lines
3.4 KiB
Ruby

require "open3"
module Streams
# Converte cover_image in MP4 slate (H.264 720p + AAC mono), allineato a infra/scripts/generate_slate.sh.
class GenerateCoverSlate
class Error < StandardError; end
SLATE_DURATION_SECS = 30
VIDEO_FILTER = "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=0x0a0a0e".freeze
def self.call(record)
new(record).call
end
def initialize(record)
@record = record
end
def call
return @record unless @record.cover_image.attached?
@image_temp = download_cover_image
@mp4_temp = encode_slate(@image_temp)
attach_slate(@mp4_temp)
write_to_slates_dir(@mp4_temp)
@record
rescue Error
raise
rescue StandardError => e
raise Error, e.message
ensure
cleanup_tempfiles
end
private
def download_cover_image
ext = extension_for(@record.cover_image.content_type)
path = temp_path("cover-in", ext)
@record.cover_image.blob.open(tmpdir: Dir.tmpdir) do |file|
FileUtils.cp(file.path, path)
end
path
end
def encode_slate(image_path)
out = temp_path("cover-slate", ".mp4")
cmd = ffmpeg_args(image_path, out)
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success? && File.exist?(out) && File.size(out).positive?
detail = stderr.to_s.strip.presence || stdout.to_s.strip
raise Error, "ffmpeg slate failed: #{detail}"
end
out
end
def ffmpeg_args(input, output)
[
"ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
"-loop", "1", "-framerate", "30", "-i", input,
"-f", "lavfi", "-i", "anullsrc=r=48000:cl=mono",
"-filter:v", VIDEO_FILTER,
"-map", "0:v", "-map", "1:a",
"-t", SLATE_DURATION_SECS.to_s,
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-profile:v", "baseline", "-level", "3.1",
"-x264-params", "keyint=30:min-keyint=30:scenecut=0:bframes=0",
"-g", "30", "-keyint_min", "30", "-force_key_frames", "expr:gte(t,n_forced*1)",
"-preset", "fast",
"-c:a", "aac", "-b:a", "128k", "-ac", "1", "-shortest", "-y", output
]
end
def attach_slate(mp4_path)
@record.cover_slate.purge if @record.cover_slate.attached?
File.open(mp4_path, "rb") do |io|
@record.cover_slate.attach(
io: io,
filename: slate_filename,
content_type: "video/mp4"
)
end
@record.reload
end
def write_to_slates_dir(mp4_path)
dest = CoverSlatePaths.expected_path(@record)
return unless dest
FileUtils.mkdir_p(File.dirname(dest))
FileUtils.cp(mp4_path, dest)
rescue Errno::EACCES, Errno::EROFS => e
Rails.logger.info("[GenerateCoverSlate] skip disk copy #{dest}: #{e.class}")
end
def slate_filename
"#{@record.class.name.underscore}-#{@record.id}.mp4"
end
def extension_for(content_type)
case content_type
when "image/png" then ".png"
when "image/webp" then ".webp"
else ".jpg"
end
end
def temp_path(prefix, ext)
path = File.join(Dir.tmpdir, "#{prefix}-#{@record.class.name}-#{@record.id}-#{SecureRandom.hex(4)}#{ext}")
@tempfiles ||= []
@tempfiles << path
path
end
def cleanup_tempfiles
Array(@tempfiles).each do |path|
FileUtils.rm_f(path)
end
end
end
end