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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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
|
||||
Reference in New Issue
Block a user