Corregge cloud-init montato, PublisherOnline multi-nodo e E2E locale-aware.
Senza volume stream-node i nodi Hetzner nascevano senza MediaMTX; sync live usa ora Client.for_session e rtmpconns (MediaMTX 1.20). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -105,6 +105,16 @@ module Mediamtx
|
||||
[]
|
||||
end
|
||||
|
||||
def list_rtmp_conns
|
||||
response = @conn.get("/v3/rtmpconns/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
|
||||
|
||||
@@ -4,17 +4,37 @@ module Mediamtx
|
||||
module_function
|
||||
|
||||
def active?(session)
|
||||
return true if rtmp_publisher?(session)
|
||||
|
||||
active_path?(path_info(session))
|
||||
end
|
||||
|
||||
def path_info(session)
|
||||
Client.new.list_paths.find { |i| i["name"] == session.mediamtx_path_name }
|
||||
Client.for_session(session).list_paths.find { |i| i["name"] == session.mediamtx_path_name }
|
||||
end
|
||||
|
||||
# MediaMTX <=1.19: online + source.type=rtmpConn.
|
||||
# MediaMTX 1.20+: online/source spesso null anche con publisher; usare rtmpconns.
|
||||
def active_path?(info)
|
||||
return false unless info
|
||||
return true if info["online"] == true && rtmp_source?(info.dig("source", "type"))
|
||||
|
||||
info["online"] == true && info.dig("source", "type") == "rtmpConn"
|
||||
false
|
||||
end
|
||||
|
||||
def rtmp_publisher?(session)
|
||||
path = session.mediamtx_path_name.to_s
|
||||
return false if path.blank?
|
||||
|
||||
Client.for_session(session).list_rtmp_conns.any? do |conn|
|
||||
conn_path = conn["path"].to_s.sub(%r{\A/}, "")
|
||||
next false unless conn_path == path
|
||||
|
||||
state = conn["state"].to_s
|
||||
state.empty? || state == "publish" || state == "idle"
|
||||
end
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def h264_video?(info)
|
||||
@@ -26,12 +46,14 @@ module Mediamtx
|
||||
end
|
||||
|
||||
def video_publishing?(session)
|
||||
info = path_info(session)
|
||||
return false unless active_path?(info)
|
||||
# Slate alwaysAvailable ha H264 ma non è il telefono.
|
||||
return false unless info.dig("source", "type") == "rtmpConn"
|
||||
return false unless active?(session)
|
||||
|
||||
info = path_info(session)
|
||||
h264_video?(info)
|
||||
end
|
||||
|
||||
def rtmp_source?(type)
|
||||
type.to_s.match?(/\Artmps?Conn\z/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ module Mediamtx
|
||||
return @session if @session.terminal?
|
||||
|
||||
path_info = Mediamtx::PublisherOnline.path_info(@session)
|
||||
publisher_online = Mediamtx::PublisherOnline.active_path?(path_info)
|
||||
publisher_online = Mediamtx::PublisherOnline.active?(@session)
|
||||
|
||||
if publisher_online
|
||||
clear_publisher_misses!(@session.id)
|
||||
@@ -86,7 +86,7 @@ module Mediamtx
|
||||
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)
|
||||
Client.for_session(session).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}")
|
||||
@@ -95,7 +95,7 @@ module Mediamtx
|
||||
def restore_slate_path!(session)
|
||||
return if session.platform == "matchlivetv"
|
||||
|
||||
Client.new.set_always_available(session, enabled: true)
|
||||
Client.for_session(session).set_always_available(session, enabled: true)
|
||||
rescue Client::Error => e
|
||||
Rails.logger.warn("[PublisherSync] enable slate session=#{session.id}: #{e.message}")
|
||||
end
|
||||
@@ -128,7 +128,7 @@ module Mediamtx
|
||||
return
|
||||
end
|
||||
|
||||
Client.new.set_path_recording(session, enabled: enabled)
|
||||
Client.for_session(session).set_path_recording(session, enabled: enabled)
|
||||
redis.set(key, desired, ex: 48.hours.to_i)
|
||||
mark_recording_patch!(session.id)
|
||||
rescue Client::Error => e
|
||||
|
||||
@@ -40,7 +40,7 @@ module Streams
|
||||
network_id = ENV["HCLOUD_NETWORK_ID"].presence
|
||||
body[:networks] = [network_id.to_i] if network_id
|
||||
user_data = cloud_init_user_data
|
||||
body[:user_data] = user_data if user_data.present?
|
||||
body[:user_data] = user_data
|
||||
|
||||
data = post("servers", body)
|
||||
server = data["server"] || {}
|
||||
@@ -114,9 +114,16 @@ module Streams
|
||||
|
||||
def cloud_init_user_data
|
||||
path = ENV["HCLOUD_USER_DATA_FILE"].presence
|
||||
return File.read(path) if path && File.file?(path)
|
||||
if path.present?
|
||||
raise Error, "HCLOUD_USER_DATA_FILE non leggibile nel container: #{path}" unless File.file?(path)
|
||||
|
||||
ENV["HCLOUD_USER_DATA"].presence
|
||||
return File.read(path)
|
||||
end
|
||||
|
||||
inline = ENV["HCLOUD_USER_DATA"].presence
|
||||
raise Error, "Manca cloud-init: imposta HCLOUD_USER_DATA_FILE (montato) o HCLOUD_USER_DATA" if inline.blank?
|
||||
|
||||
inline
|
||||
end
|
||||
|
||||
def conn
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Mediamtx::PublisherSync do
|
||||
let(:user) { User.create!(email: "sync@test.com", name: "Sync", password: "password123", role: "coach") }
|
||||
let(:user) { User.create!(email: "sync@test.com", name: "Sync", password: "Password123", role: "coach") }
|
||||
let(:club) { Club.create!(name: "Sync Club", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||
let(:team) { club.teams.create!(name: "Under 16", sport: "volleyball") }
|
||||
let!(:match) { team.matches.create!(opponent_name: "Avversario") }
|
||||
@@ -20,8 +20,11 @@ RSpec.describe Mediamtx::PublisherSync do
|
||||
before do
|
||||
Billing::AssignPlan.call(club: club, plan_slug: "premium_full")
|
||||
allow(Mediamtx::Client).to receive(:new).and_return(client)
|
||||
allow(Mediamtx::Client).to receive(:for_session).and_return(client)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:path_info).and_return(path_info)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active_path?).and_return(true)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:rtmp_publisher?).and_return(false)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active?).and_return(true)
|
||||
allow_any_instance_of(described_class).to receive(:redis).and_return(redis)
|
||||
allow(client).to receive(:set_path_recording)
|
||||
end
|
||||
@@ -36,6 +39,7 @@ RSpec.describe Mediamtx::PublisherSync do
|
||||
|
||||
it "non abilita la registrazione in connecting senza publisher online" do
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active_path?).and_return(false)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active?).and_return(false)
|
||||
path_info["online"] = false
|
||||
|
||||
described_class.new(session).call
|
||||
@@ -53,6 +57,7 @@ RSpec.describe Mediamtx::PublisherSync do
|
||||
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)
|
||||
allow(Mediamtx::PublisherOnline).to receive(:active?).and_return(false)
|
||||
|
||||
described_class.new(session).call
|
||||
|
||||
|
||||
Reference in New Issue
Block a user