Fix freeze YouTube al cambio punteggio: PNG atomica e relay stabile

rsvg scrive su .tmp poi rename; il feeder ignora PNG parziali.
YouTube relay: no doppio avvio, reconnect RTMP; overlay restart solo dopo 3 miss.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 09:07:06 +02:00
parent fde1213ca5
commit 4c7be615f1
4 changed files with 31 additions and 4 deletions

View File

@@ -37,10 +37,12 @@ module Streams
end end
def render_png!(svg_path, png_path) def render_png!(svg_path, png_path)
unless system("rsvg-convert", "-w", "1280", "-h", "720", "-o", png_path.to_s, svg_path.to_s, out: File::NULL, err: File::NULL) tmp = "#{png_path}.tmp"
unless system("rsvg-convert", "-w", "1280", "-h", "720", "-o", tmp, svg_path.to_s, out: File::NULL, err: File::NULL)
raise Error, "rsvg-convert fallito per session #{@session.id}" raise Error, "rsvg-convert fallito per session #{@session.id}"
end end
File.rename(tmp, png_path)
FileUtils.touch(png_path) FileUtils.touch(png_path)
end end
end end

View File

@@ -86,8 +86,15 @@ module Streams
end end
if running?(session.id) if running?(session.id)
Rails.logger.warn("[OverlayRelay] _air non attivo, restart session=#{session.id}") misses = redis.incr(overlay_air_miss_key(session.id)).to_i
redis.expire(overlay_air_miss_key(session.id), 120)
return if misses < 3
redis.del(overlay_air_miss_key(session.id))
Rails.logger.warn("[OverlayRelay] _air non attivo dopo #{misses} tentativi, restart session=#{session.id}")
stop(session) stop(session)
else
redis.del(overlay_air_miss_key(session.id))
end end
start(session) start(session)
@@ -181,6 +188,10 @@ module Streams
redis.get(format(REDIS_KEY, session_id)) redis.get(format(REDIS_KEY, session_id))
end end
def overlay_air_miss_key(session_id)
format("overlay_relay:air_miss:%s", session_id)
end
def process_alive?(pid) def process_alive?(pid)
Process.kill(0, pid.to_i) Process.kill(0, pid.to_i)
true true

View File

@@ -12,6 +12,8 @@ module Streams
return if session.stream_key.blank? return if session.stream_key.blank?
return if session.terminal? return if session.terminal?
return pid_for(session.id).to_i if running?(session.id)
stop(session) if pid_for(session.id).present? stop(session) if pid_for(session.id).present?
log_path = log_file(session) log_path = log_file(session)
@@ -80,6 +82,7 @@ module Streams
"-fflags", "+genpts+discardcorrupt", "-use_wallclock_as_timestamps", "1", "-fflags", "+genpts+discardcorrupt", "-use_wallclock_as_timestamps", "1",
"-analyzeduration", "10000000", "-probesize", "10000000", "-analyzeduration", "10000000", "-probesize", "10000000",
"-rw_timeout", "15000000", "-rw_timeout", "15000000",
"-reconnect", "1", "-reconnect_at_eof", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5",
"-i", intake, "-i", intake,
"-f", "lavfi", "-i", "anullsrc=channel_layout=mono:sample_rate=48000", "-f", "lavfi", "-i", "anullsrc=channel_layout=mono:sample_rate=48000",
"-map", "0:v:0", "-map", "1:a:0", "-map", "0:v:0", "-map", "1:a:0",

View File

@@ -16,13 +16,24 @@ trap("TERM") { stop = true }
trap("INT") { stop = true } trap("INT") { stop = true }
# image2pipe richiede frame continui: inviamo la PNG a ritmo fisso ( anche se invariata ). # image2pipe richiede frame continui: inviamo la PNG a ritmo fisso ( anche se invariata ).
last_bytes = nil
File.open(pipe_path, "wb") do |pipe| File.open(pipe_path, "wb") do |pipe|
loop do loop do
break if stop break if stop
if File.exist?(png_path) if File.exist?(png_path)
pipe.write(File.binread(png_path)) bytes = File.binread(png_path)
pipe.flush # Evita frame corrotti durante rsvg-convert (file parziale prima del rename atomico).
next if bytes.bytesize < 512
if bytes != last_bytes
pipe.write(bytes)
pipe.flush
last_bytes = bytes
elsif last_bytes
pipe.write(last_bytes)
pipe.flush
end
end end
sleep poll_sec sleep poll_sec