Allinea slate Cloud a AAC 48k mono e preferisce home in allocate.
Senza questo il telefono (mono 48k) veniva rifiutato sui nodi Hetzner e lo warm spare rubava le sessioni a home. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Streams
|
module Streams
|
||||||
# Assegna un StreamNode a una nuova sessione (least-loaded tra i ready).
|
# Assegna un StreamNode a una nuova sessione.
|
||||||
|
# Preferisce home finché ha slot; poi least-loaded tra i nodi overflow ready.
|
||||||
# Garantisce il nodo "home" derivato dagli ENV MediaMTX attuali.
|
# Garantisce il nodo "home" derivato dagli ENV MediaMTX attuali.
|
||||||
class NodeRegistry
|
class NodeRegistry
|
||||||
class NoCapacityError < StandardError; end
|
class NoCapacityError < StandardError; end
|
||||||
@@ -33,10 +34,16 @@ module Streams
|
|||||||
def allocate!
|
def allocate!
|
||||||
ensure_home_from_env!
|
ensure_home_from_env!
|
||||||
|
|
||||||
|
# Overflow: riempi prima home; i nodi cloud/lab sono solo quando home è pieno
|
||||||
|
# (altrimenti lo warm spare ruberebbe tutte le sessioni).
|
||||||
|
home = StreamNode.find_by(slug: HOME_SLUG)
|
||||||
|
return home if home&.allocatable?
|
||||||
|
|
||||||
node = StreamNode.ready
|
node = StreamNode.ready
|
||||||
|
.where.not(slug: HOME_SLUG)
|
||||||
.to_a
|
.to_a
|
||||||
.select(&:allocatable?)
|
.select(&:allocatable?)
|
||||||
.min_by { |n| [n.active_publishers, n.role == "home" ? 1 : 0, n.slug] }
|
.min_by { |n| [n.active_publishers, n.slug] }
|
||||||
|
|
||||||
raise NoCapacityError, "Nessun nodo streaming con slot liberi" if node.nil?
|
raise NoCapacityError, "Nessun nodo streaming con slot liberi" if node.nil?
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,27 @@ RSpec.describe Streams::NodeRegistry do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "allocates the least loaded ready node" do
|
it "prefers home while it still has free slots even if cloud is idle" do
|
||||||
|
with_env(home_env) do
|
||||||
|
home = described_class.ensure_home_from_env!
|
||||||
|
StreamNode.create!(
|
||||||
|
slug: "ingest-01",
|
||||||
|
hostname: "ingest-01.mltv-stream.net",
|
||||||
|
role: "cloud",
|
||||||
|
status: "ready",
|
||||||
|
provider: "hetzner",
|
||||||
|
rtmp_base_url: "rtmp://ingest-01.mltv-stream.net:1935",
|
||||||
|
hls_base_url: "https://ingest-01.mltv-stream.net/hls",
|
||||||
|
api_base_url: "http://10.0.0.2:9997",
|
||||||
|
max_publishers: 2,
|
||||||
|
max_relays: 2
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(described_class.allocate!).to eq(home)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allocates the least loaded cloud node when home is full" do
|
||||||
with_env(home_env) do
|
with_env(home_env) do
|
||||||
home = described_class.ensure_home_from_env!
|
home = described_class.ensure_home_from_env!
|
||||||
cloud = StreamNode.create!(
|
cloud = StreamNode.create!(
|
||||||
@@ -64,14 +84,32 @@ RSpec.describe Streams::NodeRegistry do
|
|||||||
max_publishers: 2,
|
max_publishers: 2,
|
||||||
max_relays: 2
|
max_relays: 2
|
||||||
)
|
)
|
||||||
|
cloud_busy = StreamNode.create!(
|
||||||
|
slug: "ingest-02",
|
||||||
|
hostname: "ingest-02.mltv-stream.net",
|
||||||
|
role: "cloud",
|
||||||
|
status: "ready",
|
||||||
|
provider: "hetzner",
|
||||||
|
rtmp_base_url: "rtmp://ingest-02.mltv-stream.net:1935",
|
||||||
|
hls_base_url: "https://ingest-02.mltv-stream.net/hls",
|
||||||
|
api_base_url: "http://10.0.0.3:9997",
|
||||||
|
max_publishers: 2,
|
||||||
|
max_relays: 2
|
||||||
|
)
|
||||||
|
|
||||||
user = User.create!(email: "n@example.com", name: "N", password: "Password123", role: "coach")
|
user = User.create!(email: "n@example.com", name: "N", password: "Password123", role: "coach")
|
||||||
club = Club.create!(name: "C", sport: "volleyball")
|
club = Club.create!(name: "C", sport: "volleyball")
|
||||||
team = club.teams.create!(name: "T", sport: "volleyball", slug: "t-node")
|
team = club.teams.create!(name: "T", sport: "volleyball", slug: "t-node")
|
||||||
match = team.matches.create!(opponent_name: "X", scheduled_at: 1.hour.from_now)
|
match = team.matches.create!(opponent_name: "X", scheduled_at: 1.hour.from_now)
|
||||||
|
match2 = team.matches.create!(opponent_name: "Y", scheduled_at: 2.hours.from_now)
|
||||||
|
|
||||||
|
# Riempie home (max 2)
|
||||||
|
StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "live", stream_node: home)
|
||||||
|
StreamSession.create!(match: match2, user: user, platform: "matchlivetv", status: "live", stream_node: home)
|
||||||
|
# Un publisher già su ingest-02 → least-loaded = ingest-01
|
||||||
StreamSession.create!(
|
StreamSession.create!(
|
||||||
match: match, user: user, platform: "matchlivetv", status: "live", stream_node: home
|
match: team.matches.create!(opponent_name: "Z", scheduled_at: 3.hours.from_now),
|
||||||
|
user: user, platform: "matchlivetv", status: "live", stream_node: cloud_busy
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(described_class.allocate!).to eq(cloud)
|
expect(described_class.allocate!).to eq(cloud)
|
||||||
|
|||||||
@@ -48,13 +48,12 @@ write_files:
|
|||||||
done
|
done
|
||||||
docker pull bluenviron/mediamtx:latest
|
docker pull bluenviron/mediamtx:latest
|
||||||
docker rm -f mediamtx 2>/dev/null || true
|
docker rm -f mediamtx 2>/dev/null || true
|
||||||
# Slate offline (alwaysAvailable) — richiesta da Mediamtx::Client#create_path
|
# Slate offline (alwaysAvailable) — richiesta da Mediamtx::Client#create_path.
|
||||||
# MediaMTX 1.20+: il publisher RTMP deve matchare canali audio della slate (stereo).
|
# MediaMTX 1.20+: AAC del publisher RTMP deve matchare la slate.
|
||||||
|
# Allineato ad app Android (BroadcastConfig: 48 kHz mono) e infra/scripts/generate_slate.sh.
|
||||||
mkdir -p /slates
|
mkdir -p /slates
|
||||||
if [ ! -f /slates/offline.mp4 ]; then
|
ffmpeg -y -f lavfi -i color=c=black:s=1280x720:d=2 -f lavfi -i anullsrc=r=48000:cl=mono \
|
||||||
ffmpeg -y -f lavfi -i color=c=black:s=1280x720:d=2 -f lavfi -i anullsrc=r=44100:cl=stereo \
|
-c:v libx264 -t 2 -pix_fmt yuv420p -c:a aac -ac 1 -ar 48000 -shortest /slates/offline.mp4
|
||||||
-c:v libx264 -t 2 -pix_fmt yuv420p -c:a aac -ac 2 -shortest /slates/offline.mp4
|
|
||||||
fi
|
|
||||||
# Debian cloud image: docker.io senza apparmor_parser → serve unconfined o pkg apparmor
|
# Debian cloud image: docker.io senza apparmor_parser → serve unconfined o pkg apparmor
|
||||||
docker run -d --name mediamtx --restart unless-stopped --network host \
|
docker run -d --name mediamtx --restart unless-stopped --network host \
|
||||||
--security-opt apparmor=unconfined \
|
--security-opt apparmor=unconfined \
|
||||||
|
|||||||
Reference in New Issue
Block a user