Aggiunge overlay server-side burn-in e stabilizza diretta live.

Tabellone, badge e brand sono ricodificati nel flusso _air via OverlayRelay;
sync punteggio HTTP, volume condiviso rails/sidekiq, qualità 720p migliorata
e badge CONNECTING in ripresa da pausa.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 23:55:17 +02:00
parent fd225fbadd
commit ab9cb02083
58 changed files with 2423 additions and 242 deletions

View File

@@ -12,28 +12,32 @@ module Mediamtx
end
end
def create_path(session)
path = session.mediamtx_path_name
record = session.match.team.entitlements.recording_enabled_for_mediamtx?
def create_overlay_path(session)
path = session.mediamtx_overlay_path_name
body = {
source: "publisher",
overridePublisher: true,
alwaysAvailable: true,
alwaysAvailableFile: slate_file_path,
alwaysAvailableTracks: slate_tracks,
record: record,
recordPath: "/recordings/%path/%Y-%m-%d_%H-%M-%S",
recordSegmentDuration: "60s"
record: false
}
if record
retention_hours = session.match.team.entitlements.recording_retention_days * 24 + 48
body[:recordDeleteAfter] = "#{retention_hours}h"
end
unless session.matchlivetv_platform?
body[:runOnReady] = run_on_ready_script(session)
body[:runOnReadyRestart] = true
body[:runOnNotReady] = webhook_curl(session, "disconnect")
response = @conn.post("/v3/config/paths/add/#{CGI.escape(path)}", body)
unless response.success?
err = response.body.is_a?(Hash) ? response.body["error"] : response.body
raise Error, "MediaMTX overlay path create failed: #{response.status} #{err}"
end
true
end
def create_path(session)
path = session.mediamtx_path_name
# record: false finché non c'è publisher — con alwaysAvailable MediaMTX registrerebbe
# solo la slate (schermo nero) in pausa/attesa.
body = recording_body(session, enabled: false).merge(
source: "publisher",
overridePublisher: true,
alwaysAvailable: true,
alwaysAvailableFile: slate_file_path
)
# YouTube: relay continuo gestito da Streams::YoutubeRelay (ffmpeg in sidekiq/rails).
response = @conn.post("/v3/config/paths/add/#{CGI.escape(path)}", body)
unless response.success?
err = response.body.is_a?(Hash) ? response.body["error"] : response.body
@@ -44,18 +48,29 @@ module Mediamtx
def delete_path(session)
delete_path_name(session.mediamtx_path_name)
delete_path_name(session.mediamtx_overlay_path_name)
end
def delete_path_name(path_name)
@conn.post("/v3/config/paths/delete/#{CGI.escape(path_name)}")
end
def set_path_recording(session, enabled:)
path = session.mediamtx_path_name
body = recording_body(session, enabled: enabled)
response = @conn.patch("/v3/config/paths/patch/#{CGI.escape(path)}", body)
unless response.success?
err = response.body.is_a?(Hash) ? response.body["error"] : response.body
raise Error, "MediaMTX recording patch failed: #{response.status} #{err}"
end
true
end
def patch_path(session)
path = session.mediamtx_path_name
body = {
alwaysAvailable: true,
alwaysAvailableFile: slate_file_path,
alwaysAvailableTracks: slate_tracks
alwaysAvailableFile: slate_file_path
}
response = @conn.patch("/v3/config/paths/patch/#{CGI.escape(path)}", body)
unless response.success?
@@ -81,6 +96,21 @@ module Mediamtx
private
def recording_body(session, enabled:)
ent = session.match.team.entitlements
can_record = ent.recording_enabled_for_mediamtx?
body = {
record: enabled && can_record,
recordPath: "/recordings/%path/%Y-%m-%d_%H-%M-%S",
recordSegmentDuration: "60s"
}
if enabled && can_record
retention_hours = ent.recording_retention_days * 24 + 48
body[:recordDeleteAfter] = "#{retention_hours}h"
end
body
end
def publish_webhook(session)
webhook_curl(session, "connect")
end
@@ -122,12 +152,5 @@ module Mediamtx
def slate_file_path
ENV.fetch("MEDIAMTX_SLATE_FILE", "/slates/offline.mp4")
end
def slate_tracks
[
{ codec: "H264" },
{ codec: "MPEG4Audio", sampleRate: 48_000, channelCount: 2 }
]
end
end
end

View File

@@ -0,0 +1,51 @@
module Mediamtx
# MediaMTX in produzione è distroless: runOnReady con wget non funziona.
# Sincronizziamo lo stato Rails leggendo l'API paths (poll da /live/:id/status.json).
class PublisherSync
def initialize(session)
@session = session
end
def call
return @session if @session.terminal?
info = Mediamtx::Client.new.list_paths.find { |i| i["name"] == @session.mediamtx_path_name }
publisher_online = info && info["online"]
if publisher_online
if @session.paused?
# RTMP ancora connesso in pausa: non forzare live/reconnect.
elsif @session.may_go_live?
@session.go_live!
Streams::Overlay::Refresh.call(@session.reload)
elsif @session.reconnecting? && @session.may_reconnect?
@session.reconnect!
cancel_timeout
Streams::Overlay::Refresh.call(@session.reload)
end
elsif !@session.paused? && @session.live? && @session.may_lose_connection?
@session.lose_connection!
schedule_timeout unless @session.paused?
end
@session.reload
end
private
def cancel_timeout
return if @session.timeout_job_id.blank?
Sidekiq::ScheduledSet.new.find_job(@session.timeout_job_id)&.delete
@session.update!(timeout_job_id: nil)
end
def schedule_timeout
job = DisconnectionTimeoutJob.perform_in(
MatchLiveTv.reconnect_timeout_seconds.seconds,
@session.id
)
@session.update!(timeout_job_id: job)
end
end
end