Mantiene la copertina YouTube se il telefono cade e avvia ffmpeg sul nodo, non via SSH.

Il relay HLS→RTMPS resta sul worker/agent del nodo assegnato, così tre dirette contemporanee restano in onda sul sito e sul canale della società senza schermo nero.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-16 20:41:46 +02:00
co-authored by Cursor
parent bfb3115a64
commit baa6283a15
36 changed files with 1444 additions and 141 deletions
@@ -23,6 +23,7 @@ RSpec.describe "Streams::NodeProvisioner cloud" do
ENV["HLS_PUBLIC_URL"] = "https://home.example/hls"
ENV["STREAM_CLOUD_DNS_SUFFIX"] = "mltv-stream.net"
ENV["STREAM_CLOUD_MAX_PUBLISHERS"] = "4"
ENV["STREAM_CLOUD_PUBLIC_CONTROL"] = "0"
node = Streams::NodeProvisioner.new(cloud: cloud, dns: dns).provision_cloud!
expect(node.slug).to eq("ingest-01")
@@ -31,11 +32,13 @@ RSpec.describe "Streams::NodeProvisioner cloud" do
expect(node.hostname).to eq("ingest-01.mltv-stream.net")
expect(node.rtmp_base_url).to eq("rtmp://ingest-01.mltv-stream.net:1935")
expect(node.api_base_url).to eq("http://10.0.0.9:9997")
expect(node.hls_base_url).to eq("https://home.example/hls")
expect(node.internal_hls_url).to eq("http://10.0.0.9:8888")
expect(dns).to have_received(:upsert_a).with("ingest-01.mltv-stream.net", "49.13.9.9")
ensure
%w[
MEDIAMTX_API_URL MEDIAMTX_RTMP_URL HLS_PUBLIC_URL
STREAM_CLOUD_DNS_SUFFIX STREAM_CLOUD_MAX_PUBLISHERS
STREAM_CLOUD_DNS_SUFFIX STREAM_CLOUD_MAX_PUBLISHERS STREAM_CLOUD_PUBLIC_CONTROL
].each { |k| ENV.delete(k) }
end
end
@@ -27,7 +27,8 @@ RSpec.describe Streams::NodeProvisioner do
"MEDIAMTX_API_URL" => "http://mtx-home:9997",
"MEDIAMTX_RTMP_URL" => "rtmp://ingest-home.example:1935",
"HLS_PUBLIC_URL" => "https://ingest-home.example/hls",
"STREAM_LAB_MAX_PUBLISHERS" => "2"
"STREAM_LAB_MAX_PUBLISHERS" => "2",
"MEDIAMTX_LAB_API_URL" => nil
) do
provisioner = described_class.new
node = provisioner.provision_lab!
@@ -45,13 +46,35 @@ RSpec.describe Streams::NodeProvisioner do
end
end
it "attaches a dedicated lab MediaMTX when MEDIAMTX_LAB_API_URL is set" do
with_env(
"STREAM_CLOUD_PROVIDER" => "local_lab",
"STREAM_DNS_PROVIDER" => "lab",
"MEDIAMTX_API_URL" => "http://mtx-home:9997",
"MEDIAMTX_RTMP_URL" => "rtmp://ingest-home.example:1935",
"HLS_PUBLIC_URL" => "https://ingest-home.example/hls",
"MEDIAMTX_LAB_API_URL" => "http://mediamtx_lab:9997",
"MEDIAMTX_LAB_HLS_URL" => "http://mediamtx_lab:8888",
"MEDIAMTX_LAB_INTERNAL_RTMP_URL" => "rtmp://mediamtx_lab:1935",
"MEDIAMTX_LAB_RTMP_URL" => "rtmp://127.0.0.1:11935"
) do
node = described_class.new.provision_lab!
expect(node.api_base_url).to eq("http://mediamtx_lab:9997")
expect(node.internal_rtmp_url).to eq("rtmp://mediamtx_lab:1935")
expect(node.internal_hls_url).to eq("http://mediamtx_lab:8888")
expect(node.rtmp_base_url).to eq("rtmp://127.0.0.1:11935")
expect(node.hls_base_url).to eq("https://ingest-home.example/hls")
end
end
it "refuses to decommission a busy node" do
with_env(
"STREAM_CLOUD_PROVIDER" => "local_lab",
"STREAM_DNS_PROVIDER" => "lab",
"MEDIAMTX_API_URL" => "http://mtx-home:9997",
"MEDIAMTX_RTMP_URL" => "rtmp://ingest-home.example:1935",
"HLS_PUBLIC_URL" => "https://ingest-home.example/hls"
"HLS_PUBLIC_URL" => "https://ingest-home.example/hls",
"MEDIAMTX_LAB_API_URL" => nil
) do
node = described_class.new.provision_lab!
user = User.create!(email: "lab@example.com", name: "L", password: "Password123", role: "coach")
@@ -18,10 +18,57 @@ RSpec.describe Streams::YoutubeRelay do
it "remuxes HLS with AAC bitstream filter for FLV" do
cmd = args(:hls, "http://mediamtx:8888/live/match_x/index.m3u8")
expect(cmd).to include("-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc", "-f", "flv")
expect(cmd).to include("-reconnect", "1")
expect(cmd).not_to include("-b:a")
end
end
describe "node queue affinity" do
def with_env(vars)
previous = vars.keys.index_with { |k| ENV[k] }
vars.each { |k, v| ENV[k] = v }
yield
ensure
previous.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
end
let(:session_id) { SecureRandom.uuid }
let(:lab_node) { instance_double(StreamNode, slug: "ingest-lab-01") }
def session_double(stream_node: nil)
instance_double(
StreamSession,
id: session_id,
platform: "youtube",
terminal?: false,
stream_key: "yt-key",
status: "live",
stream_node: stream_node,
mediamtx_path_name: "live/match_#{session_id}",
mediamtx_internal_rtmp_url: "rtmp://10.0.0.9:1935",
mediamtx_internal_hls_url: "http://10.0.0.9:8888"
)
end
it "routes home sessions to youtube_relay_home" do
expect(described_class.queue_for(session_double)).to eq("youtube_relay_home")
end
it "routes overflow sessions to the node slug queue" do
expect(described_class.queue_for(session_double(stream_node: lab_node))).to eq("youtube_relay_ingest-lab-01")
end
it "requeues ensure to the assigned node when the local worker does not match" do
with_env("STREAM_NODE_SLUG" => "home", "YOUTUBE_RELAY_WORKER" => "1") do
allow(described_class).to receive(:worker?).and_return(true)
job_proxy = double("ConfiguredJob", perform_later: true)
expect(YoutubeRelayEnsureJob).to receive(:set).with(hash_including(queue: "youtube_relay_ingest-lab-01", wait: 2.seconds)).and_return(job_proxy)
expect(described_class.ensure_on_worker!(session_double(stream_node: lab_node))).to eq(:wrong_host)
end
end
end
describe "multi-host sticky stop/capacity" do
let(:redis) { Redis.new(url: ENV.fetch("REDIS_URL", "redis://redis:6379/0")) }
let(:session_id) { SecureRandom.uuid }
@@ -30,6 +77,7 @@ RSpec.describe Streams::YoutubeRelay do
allow(described_class).to receive(:worker?).and_return(true)
allow(described_class).to receive(:worker_id).and_return("worker-a")
allow(described_class).to receive(:max_concurrent).and_return(1)
allow(described_class).to receive(:local_node_slug).and_return("home")
redis.flushdb
end
@@ -40,7 +88,8 @@ RSpec.describe Streams::YoutubeRelay do
platform: "youtube",
terminal?: false,
stream_key: "yt-key",
status: "live"
status: "live",
stream_node: nil
)
end
@@ -52,15 +101,91 @@ RSpec.describe Streams::YoutubeRelay do
expect(redis.get(format(Streams::YoutubeRelay::OWNER_KEY, session_id))).to eq("worker-b")
end
it "requeues ensure when at capacity" do
it "requeues ensure on the node queue when at capacity" do
other_id = SecureRandom.uuid
redis.sadd(format(Streams::YoutubeRelay::OWNED_SET, "worker-a"), other_id)
allow(described_class).to receive(:intake_available?).and_return(true)
expect(YoutubeRelayEnsureJob).to receive(:set).with(hash_including(wait: 5.seconds, queue: :youtube_relay)).and_return(
expect(YoutubeRelayEnsureJob).to receive(:set).with(hash_including(wait: 5.seconds, queue: "youtube_relay_home")).and_return(
double(perform_later: true)
)
expect(described_class.ensure_on_worker!(session_double)).to eq(:at_capacity)
end
end
describe ".mediamtx_intake_source (private)" do
def with_env(vars)
previous = vars.keys.index_with { |k| ENV[k] }
vars.each { |k, v| ENV[k] = v }
yield
ensure
previous.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
end
let(:session) do
instance_double(
StreamSession,
stream_node: instance_double(StreamNode, slug: "ingest-lab-01"),
mediamtx_path_name: "live/match_abc",
mediamtx_internal_rtmp_url: "rtmp://10.0.0.9:1935",
mediamtx_internal_hls_url: "http://10.0.0.9:8888"
)
end
it "uses loopback RTMP when the worker is on the assigned node" do
with_env(
"STREAM_NODE_SLUG" => "ingest-lab-01",
"STREAM_NODE_LOCAL_RTMP_URL" => "rtmp://127.0.0.1:1935"
) do
allow(Mediamtx::PublisherOnline).to receive(:active?).with(session).and_return(true)
expect(described_class.send(:mediamtx_intake_source, session)).to eq(
[:rtmp, "rtmp://127.0.0.1:1935/live/match_abc"]
)
end
end
it "uses loopback when the node relay agent is enabled" do
with_env(
"STREAM_NODE_SLUG" => "ingest-lab-01",
"STREAM_NODE_RELAY_AGENT_URL" => "http://203.0.113.10:9100",
"STREAM_NODE_LOCAL_RTMP_URL" => "rtmp://mediamtx_lab:1935"
) do
allow(Mediamtx::PublisherOnline).to receive(:active?).with(session).and_return(true)
expect(described_class.send(:mediamtx_intake_source, session)).to eq(
[:rtmp, "rtmp://127.0.0.1:1935/live/match_abc"]
)
end
end
end
describe "relay agent spawn" do
def with_env(vars)
previous = vars.keys.index_with { |k| ENV[k] }
vars.each { |k, v| ENV[k] = v }
yield
ensure
previous.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
end
it "asks the node agent to start ffmpeg and stores the pid" do
session = instance_double(
StreamSession,
id: "sess-1",
mediamtx_path_name: "live/match_abc",
stream_key: "yt-key"
)
allow(described_class).to receive(:log_file).and_return("/tmp/youtube_relay_test.log")
allow(File).to receive(:write)
expect(described_class).to receive(:agent_request).with(
Net::HTTP::Post,
"/relays",
hash_including("session_id" => "sess-1", "path" => "live/match_abc")
).and_return("pid" => 4242)
pid = with_env("STREAM_NODE_RELAY_AGENT_URL" => "http://203.0.113.10:9100") do
described_class.send(:start_via_agent!, session)
end
expect(pid).to eq(4242)
end
end
end