Riduce reload MediaMTX in live e allinea ingest mobile al preset qualità.
Evita patch recording ripetute che distruggevano i muxer HLS (500 ops) e fa encoderare 1080p/720p con codec e bitrate coerenti al wizard. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,8 @@ 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
|
||||
RECORDING_PATCH_COOLDOWN = -> { ENV.fetch("MEDIAMTX_RECORDING_PATCH_COOLDOWN_SECS", "45").to_i.seconds }
|
||||
|
||||
def initialize(session)
|
||||
@session = session
|
||||
end
|
||||
@@ -9,17 +11,15 @@ module Mediamtx
|
||||
def call
|
||||
return @session if @session.terminal?
|
||||
|
||||
info = Mediamtx::Client.new.list_paths.find { |i| i["name"] == @session.mediamtx_path_name }
|
||||
publisher_online = Mediamtx::PublisherOnline.active_path?(info)
|
||||
path_info = Mediamtx::PublisherOnline.path_info(@session)
|
||||
publisher_online = Mediamtx::PublisherOnline.active_path?(path_info)
|
||||
|
||||
if publisher_online
|
||||
clear_publisher_misses!(@session.id)
|
||||
if @session.paused?
|
||||
# RTMP ancora connesso in pausa: non forzare live/reconnect.
|
||||
sync_path_recording!(@session, enabled: false)
|
||||
else
|
||||
sync_path_recording!(@session, enabled: true)
|
||||
enable_live_path!(@session)
|
||||
enable_live_path_once!(@session)
|
||||
if @session.may_go_live?
|
||||
@session.go_live!
|
||||
@session.reload
|
||||
@@ -34,17 +34,18 @@ module Mediamtx
|
||||
elsif !@session.paused? && @session.live? && @session.may_lose_connection?
|
||||
if publisher_offline_sustained?(@session.id)
|
||||
clear_publisher_misses!(@session.id)
|
||||
sync_path_recording!(@session, enabled: false)
|
||||
disable_recording_on_path!(@session, path_info: path_info)
|
||||
restore_slate_path!(@session)
|
||||
@session.lose_connection!
|
||||
schedule_timeout unless @session.paused?
|
||||
end
|
||||
elsif !publisher_online && @session.status.in?(%w[reconnecting connecting]) && !@session.paused?
|
||||
sync_path_recording!(@session, enabled: false)
|
||||
# Non togliere recording qui: un gap RTMP breve non deve ricaricare la config MediaMTX.
|
||||
restore_slate_path!(@session)
|
||||
end
|
||||
|
||||
sync_path_recording!(@session, enabled: false) if @session.paused? && !publisher_online
|
||||
desired_recording = recording_should_be_active?(publisher_online)
|
||||
sync_path_recording!(@session, enabled: desired_recording, path_info: path_info)
|
||||
|
||||
@session.reload.tap do |session|
|
||||
schedule_youtube_relay_if_needed(session)
|
||||
@@ -79,14 +80,15 @@ module Mediamtx
|
||||
Youtube::LivePipeline.schedule!(session, force: force)
|
||||
end
|
||||
|
||||
# Match Live TV: lascia alwaysAvailable attivo — MediaMTX usa il publisher quando c'è
|
||||
# e la copertina nei gap RTMP (telefono instabile). Disattivare la slate lasciava HLS
|
||||
# senza stream ("no stream is available") e il player web in pausa/spinner.
|
||||
def enable_live_path!(session)
|
||||
def enable_live_path_once!(session)
|
||||
return unless session.platform == "youtube"
|
||||
|
||||
key = format("youtube:slate_disabled:%s", session.id)
|
||||
return unless redis.set(key, "1", nx: true, ex: 48.hours.to_i)
|
||||
|
||||
Client.new.set_always_available(session, enabled: false)
|
||||
rescue Client::Error => e
|
||||
redis.del(format("youtube:slate_disabled:%s", session.id))
|
||||
Rails.logger.warn("[PublisherSync] disable slate session=#{session.id}: #{e.message}")
|
||||
end
|
||||
|
||||
@@ -98,23 +100,62 @@ module Mediamtx
|
||||
Rails.logger.warn("[PublisherSync] enable slate session=#{session.id}: #{e.message}")
|
||||
end
|
||||
|
||||
# In produzione i webhook MediaMTX (wget) non girano: la registrazione va accesa qui.
|
||||
def sync_path_recording!(session, enabled:)
|
||||
def recording_should_be_active?(publisher_online)
|
||||
return false unless @session.match.team.entitlements.recording_enabled_for_mediamtx?
|
||||
return false if @session.paused? || @session.terminal?
|
||||
return false unless publisher_online
|
||||
return false unless @session.status.in?(%w[live reconnecting])
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def disable_recording_on_path!(session, path_info:)
|
||||
sync_path_recording!(session, enabled: false, path_info: path_info, force: true)
|
||||
end
|
||||
|
||||
# In produzione i webhook MediaMTX (wget) non girano: la registrazione va accesa al primo go-live.
|
||||
# Evitiamo patch ripetute: ogni reload config distrugge i muxer HLS (500 sul proxy /hls/).
|
||||
def sync_path_recording!(session, enabled:, path_info: nil, force: false)
|
||||
return unless session.match.team.entitlements.recording_enabled_for_mediamtx?
|
||||
|
||||
key = recording_state_key(session.id)
|
||||
desired = enabled ? "1" : "0"
|
||||
return if redis.get(key) == desired
|
||||
return if path_info && path_recording_active?(path_info) == enabled
|
||||
|
||||
if !enabled && !force && recording_patch_on_cooldown?(session.id)
|
||||
Rails.logger.debug("[PublisherSync] skip recording patch session=#{session.id} (cooldown)")
|
||||
return
|
||||
end
|
||||
|
||||
Client.new.set_path_recording(session, enabled: enabled)
|
||||
redis.set(key, desired, ex: 48.hours.to_i)
|
||||
mark_recording_patch!(session.id)
|
||||
rescue Client::Error => e
|
||||
Rails.logger.warn("[PublisherSync] recording enabled=#{enabled} session=#{session.id}: #{e.message}")
|
||||
end
|
||||
|
||||
def path_recording_active?(path_info)
|
||||
path_info["record"] == true
|
||||
end
|
||||
|
||||
def recording_patch_on_cooldown?(session_id)
|
||||
last = redis.get(recording_patch_at_key(session_id)).to_i
|
||||
last.positive? && Time.at(last) > RECORDING_PATCH_COOLDOWN.call.ago
|
||||
end
|
||||
|
||||
def mark_recording_patch!(session_id)
|
||||
redis.set(recording_patch_at_key(session_id), Time.current.to_i, ex: 48.hours.to_i)
|
||||
end
|
||||
|
||||
def recording_patch_at_key(session_id)
|
||||
format("mediamtx:recording:patched_at:%s", session_id)
|
||||
end
|
||||
|
||||
def self.forget_recording_state!(session_id)
|
||||
redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
|
||||
redis.del("mediamtx:recording:#{session_id}")
|
||||
redis.del(format("mediamtx:recording:patched_at:%s", session_id))
|
||||
rescue Redis::BaseError
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ module Sessions
|
||||
# Sceglie il preset più alto supportato dall'upload misurato (test rete).
|
||||
class SelectQuality
|
||||
UPLOAD_HEADROOM = 0.8
|
||||
UPLOAD_HEADROOM_1080P = 1.0
|
||||
|
||||
PRESETS = [
|
||||
{ id: "1080p_30_4.5mbps", target_bitrate: 4_500_000, target_fps: 30 },
|
||||
@@ -23,7 +24,8 @@ module Sessions
|
||||
end
|
||||
|
||||
def upload_sufficient?(preset)
|
||||
@upload_mbps >= (preset[:target_bitrate] / 1_000_000.0 * UPLOAD_HEADROOM)
|
||||
headroom = preset[:id].start_with?("1080p") ? UPLOAD_HEADROOM_1080P : UPLOAD_HEADROOM
|
||||
@upload_mbps >= (preset[:target_bitrate] / 1_000_000.0 * headroom)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,30 +13,66 @@ RSpec.describe Mediamtx::PublisherSync do
|
||||
end
|
||||
let(:client) { instance_double(Mediamtx::Client) }
|
||||
let(:redis) { instance_double(Redis, get: nil, set: true, incr: 1, expire: true, del: 1) }
|
||||
let(:path_info) do
|
||||
{ "name" => session.mediamtx_path_name, "online" => true, "source" => { "type" => "rtmpConn" }, "record" => false }
|
||||
end
|
||||
|
||||
before do
|
||||
Billing::AssignPlan.call(club: club, plan_slug: "premium_full")
|
||||
allow(Mediamtx::Client).to receive(:new).and_return(client)
|
||||
allow(client).to receive_messages(list_paths: [ {
|
||||
"name" => session.mediamtx_path_name, "online" => true, "source" => { "type" => "rtmpConn" }
|
||||
} ])
|
||||
allow(Mediamtx::PublisherOnline).to receive(:path_info).and_return(path_info)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active_path?).and_return(true)
|
||||
allow_any_instance_of(described_class).to receive(:redis).and_return(redis)
|
||||
allow(client).to receive(:set_path_recording)
|
||||
end
|
||||
|
||||
it "abilita la registrazione quando il publisher è online" do
|
||||
it "abilita la registrazione quando il publisher è online e la sessione è live" do
|
||||
session.update!(status: "live")
|
||||
|
||||
described_class.new(session).call
|
||||
|
||||
expect(client).to have_received(:set_path_recording).with(session, enabled: true)
|
||||
end
|
||||
|
||||
it "disabilita la registrazione in pausa" do
|
||||
session.update!(status: "paused")
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active_path?).and_return(true)
|
||||
it "non abilita la registrazione in connecting senza publisher online" do
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active_path?).and_return(false)
|
||||
path_info["online"] = false
|
||||
|
||||
described_class.new(session).call
|
||||
|
||||
expect(client).to have_received(:set_path_recording).with(session, enabled: false)
|
||||
expect(client).not_to have_received(:set_path_recording)
|
||||
end
|
||||
|
||||
it "abilita la registrazione quando connecting diventa live con publisher online" do
|
||||
described_class.new(session).call
|
||||
|
||||
expect(session.reload.status).to eq("live")
|
||||
expect(client).to have_received(:set_path_recording).with(session, enabled: true)
|
||||
end
|
||||
|
||||
it "non disabilita la registrazione su reconnecting con publisher offline" do
|
||||
session.update!(status: "reconnecting")
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active_path?).and_return(false)
|
||||
|
||||
described_class.new(session).call
|
||||
|
||||
expect(client).not_to have_received(:set_path_recording)
|
||||
end
|
||||
|
||||
it "non patcha la registrazione in pausa (gestita da Sessions::Pause)" do
|
||||
session.update!(status: "paused")
|
||||
|
||||
described_class.new(session).call
|
||||
|
||||
expect(client).not_to have_received(:set_path_recording)
|
||||
end
|
||||
|
||||
it "salta la patch se MediaMTX ha già lo stato record desiderato" do
|
||||
session.update!(status: "live")
|
||||
path_info["record"] = true
|
||||
|
||||
described_class.new(session).call
|
||||
|
||||
expect(client).not_to have_received(:set_path_recording)
|
||||
end
|
||||
end
|
||||
|
||||
14
backend/spec/services/sessions/select_quality_spec.rb
Normal file
14
backend/spec/services/sessions/select_quality_spec.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Sessions::SelectQuality do
|
||||
it "richiede più upload per 1080p rispetto a 720p" do
|
||||
expect(described_class.call(upload_mbps: 4.4)[:id]).to eq("720p_30_4mbps")
|
||||
expect(described_class.call(upload_mbps: 5.0)[:id]).to eq("1080p_30_4.5mbps")
|
||||
end
|
||||
|
||||
it "consente 1080p solo con upload sufficiente" do
|
||||
preset = described_class.call(upload_mbps: 4.5)
|
||||
|
||||
expect(preset[:id]).to eq("1080p_30_4.5mbps")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user