Files
MatchLiveTv/backend/app/services/mediamtx/client.rb
Emiliano Frascaro f3ff657fc2 Sposta overlay video sull'app Android e rimuove ffmpeg server-side.
Il tabellone stile SportCam e il watermark Match Live TV sono bruciati in GPU via RootEncoder; sul backend spariscono OverlayRelay, job Sidekiq e volume stream_overlays.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 18:12:35 +02:00

186 lines
5.9 KiB
Ruby

require "set"
module Mediamtx
class Client
class Error < StandardError; end
def initialize(base_url: MatchLiveTv.mediamtx_api_url)
@conn = Faraday.new(url: base_url) do |f|
f.request :json
f.response :json
f.adapter Faraday.default_adapter
end
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.
# YouTube: niente slate sul path camera (maschera il video al relay ffmpeg).
body = recording_body(session, enabled: false).merge(
source: "publisher",
overridePublisher: true
)
body[:alwaysAvailable] = true
body[:alwaysAvailableFile] = slate_file_path
# YouTube: telefono → MediaMTX; relay copy verso RTMPS in sidekiq.
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 path create failed: #{response.status} #{err}"
end
remember_always_available(path, enabled: true)
true
end
def delete_path(session)
delete_path_name(session.mediamtx_path_name)
delete_path_name(session.mediamtx_overlay_path_name)
forget_always_available(session.mediamtx_path_name)
end
def delete_path_name(path_name)
@conn.post("/v3/config/paths/delete/#{CGI.escape(path_name)}")
forget_always_available(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)
set_always_available(session, enabled: true)
end
# Slate alwaysAvailable: copertina sullo stesso path quando il telefono è offline.
# Disattivare quando il publisher è in onda; riattivare in pausa/disconnessione.
def set_always_available(session, enabled:)
path = session.mediamtx_path_name
return true if always_available_remembered?(path, enabled: enabled)
body = if enabled
{ alwaysAvailable: true, alwaysAvailableFile: slate_file_path }
else
{ alwaysAvailable: false }
end
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 alwaysAvailable patch failed: #{response.status} #{err}"
end
remember_always_available(path, enabled: enabled)
true
end
def list_paths
response = @conn.get("/v3/paths/list")
return [] unless response.success?
body = response.body
body.is_a?(Hash) ? (body["items"] || []) : []
rescue Error, Faraday::Error
[]
end
def online_path_names
Set.new(list_paths.filter_map { |item| item["name"] if item["online"] })
end
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
def disconnect_webhook(session)
webhook_curl(session, "disconnect")
end
def webhook_curl(session, event)
secret = MatchLiveTv.mediamtx_webhook_secret
rails = ENV.fetch("RAILS_WEBHOOK_URL", "http://rails:3000")
payload = %({"session_id":"#{session.id}"})
# MediaMTX image non include curl; wget è disponibile nell'immagine ufficiale
<<~SCRIPT.squish
wget -q -O- --post-data='#{payload}' --header='Content-Type: application/json'
--header="X-MediaMTX-Signature: $(printf '%s' '#{payload}' | openssl dgst -sha256 -hmac '#{secret}' | cut -d' ' -f2)"
#{rails}/webhooks/mediamtx/#{event}
SCRIPT
end
def run_on_ready_script(session)
connect = webhook_curl(session, "connect")
return connect if session.matchlivetv_platform?
"#{connect} & #{relay_script(session)}"
end
def relay_script(session)
return "echo 'youtube relay skipped'" unless session.platform == "youtube"
key = session.stream_key
path = session.mediamtx_path_name
<<~SCRIPT.squish
ffmpeg -re -i rtmp://127.0.0.1:1935/#{path} -c copy -f flv
rtmp://a.rtmp.youtube.com/live2/#{key} 2>/var/log/ffmpeg-#{path}.log
SCRIPT
end
def slate_file_path
ENV.fetch("MEDIAMTX_SLATE_FILE", "/slates/offline.mp4")
end
def always_available_remembered?(path, enabled:)
redis.get(always_available_key(path)) == always_available_value(enabled)
rescue Redis::BaseError
false
end
def remember_always_available(path, enabled:)
redis.set(always_available_key(path), always_available_value(enabled), ex: 48.hours.to_i)
rescue Redis::BaseError
nil
end
def forget_always_available(path)
redis.del(always_available_key(path))
rescue Redis::BaseError
nil
end
def always_available_key(path)
"mediamtx:always_available:#{path}"
end
def always_available_value(enabled)
enabled ? "1" : "0"
end
def redis
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
end
end
end