# frozen_string_literal: true namespace :streams do namespace :nodes do desc "Assicura il nodo home dagli ENV MediaMTX" task ensure_home: :environment do node = Streams::NodeRegistry.ensure_home_from_env! puts "home ready slug=#{node.slug} rtmp=#{node.rtmp_base_url} max=#{node.max_publishers}" end desc "Provisiona un nodo lab (STREAM_CLOUD_PROVIDER=local_lab|proxmox_lab)" task provision_lab: :environment do node = Streams::NodeProvisioner.new.provision_lab! puts "lab node ready slug=#{node.slug} host=#{node.hostname} id=#{node.provider_instance_id}" if ENV.fetch("STREAM_DNS_PROVIDER", "lab") == "lab" puts "DNS lab snippet:" puts Streams::DnsProviders::Lab.new.hosts_file_snippet end end desc "Provisiona un nodo Hetzner Cloud + DNS mltv-stream.net (richiede HCLOUD_TOKEN)" task provision_cloud: :environment do node = Streams::NodeProvisioner.new.provision_cloud! puts "cloud node ready slug=#{node.slug} host=#{node.hostname} id=#{node.provider_instance_id} ip=#{node.metadata['public_ip']}" end desc "Dump record DNS lab (Redis)" task dns_lab_dump: :environment do puts Streams::DnsProviders::Lab.new.hosts_file_snippet end end namespace :relay do desc "Mostra coda e slug nodo per i worker ffmpeg YouTube" task queues: :environment do puts "local_slug=#{Streams::YoutubeRelay.local_node_slug} worker=#{Streams::YoutubeRelay.worker?}" StreamNode.order(:slug).find_each do |node| puts "node=#{node.slug} queue=youtube_relay_#{node.slug} status=#{node.status} relays=#{node.max_relays}" end end desc "Simula un telefono RTMP e verifica che ffmpeg parta sul worker del nodo lab" task simulate: :environment do abort "Esegui questo task nel container Rails (docker compose exec rails ...)" unless File.exist?("/.dockerenv") || ENV["FORCE_RELAY_SIMULATE"] == "1" home = Streams::NodeRegistry.ensure_home_from_env! lab = StreamNode.find_by(slug: ENV.fetch("STREAM_NODE_SLUG", "ingest-lab-01")) lab ||= Streams::NodeProvisioner.new.provision_lab! user = User.find_by!(email: ENV.fetch("E2E_USER_EMAIL", "coach@matchlivetv.test")) team = user.teams.first || user.clubs.first&.teams&.first || Team.first abort "Nessun team per #{user.email}" unless team match = team.matches.first || team.matches.create!(opponent_name: "Relay sim") previous_max = home.max_publishers filler = nil session = nil ffmpeg_pid = nil begin home.update!(max_publishers: 1, status: "ready") filler = occupy_home!(home, user, match) session = build_lab_youtube_session!(lab, user, match) Mediamtx::Client.for_session(session).create_path(session) session.begin_connect! if session.may_begin_connect? session.update!(status: "connecting") unless session.status.in?(%w[connecting live reconnecting]) puts "filler_home=#{filler.id} lab_session=#{session.id} node=#{session.stream_node.slug} queue=#{Streams::YoutubeRelay.queue_for(session)}" ffmpeg_pid = spawn_test_rtmp!(session) wait_for!("publisher MediaMTX", 30) { Mediamtx::PublisherOnline.active?(session.reload) } Streams::YoutubeRelay.enqueue_ensure!(session) wait_for!("owner Redis sul nodo lab", 25) do owner_for(session.id) == lab.slug end owner = owner_for(session.id) home_owns = relay_redis.sismember(format(Streams::YoutubeRelay::OWNED_SET, "home"), session.id) puts "owner=#{owner} home_owns=#{home_owns} queue=#{Streams::YoutubeRelay.queue_for(session)}" abort "FAIL: owner atteso #{lab.slug}, trovato #{owner.inspect}" unless owner == lab.slug abort "FAIL: il worker home ha reclamato la sessione lab" if home_owns puts "OK — ffmpeg assegnato al worker #{lab.slug}, non a home" ensure begin Process.kill("TERM", ffmpeg_pid) if ffmpeg_pid rescue Errno::ESRCH nil end session&.update!(status: "ended", ended_at: Time.current) if session&.persisted? filler&.update!(status: "ended", ended_at: Time.current) if filler&.persisted? home.update!(max_publishers: previous_max) end end end end def occupy_home!(home, user, match) StreamSession.create!( match: match, user: user, platform: "matchlivetv", status: "live", privacy_status: "unlisted", stream_node: home, publish_token: SecureRandom.hex(8), started_at: Time.current ) end def build_lab_youtube_session!(lab, user, match) StreamSession.create!( match: match, user: user, platform: "youtube", status: "idle", privacy_status: "unlisted", stream_node: lab, publish_token: SecureRandom.hex(8), stream_key: "e2e-dummy-#{SecureRandom.hex(6)}" ) end def spawn_test_rtmp!(session) intake = "#{session.mediamtx_internal_rtmp_url.chomp('/')}/#{session.mediamtx_path_name}" cmd = [ "ffmpeg", "-hide_banner", "-loglevel", "error", "-re", "-f", "lavfi", "-i", "testsrc=size=1280x720:rate=30", "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000", "-t", "40", "-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p", "-b:v", "1500k", "-c:a", "aac", "-ac", "1", "-ar", "48000", "-b:a", "96k", "-f", "flv", intake ] pid = Process.spawn(*cmd, %i[out err] => "/tmp/relay_sim_ffmpeg.log") Process.detach(pid) puts "telefono simulato pid=#{pid} rtmp=#{intake}" pid end def wait_for!(label, timeout_sec) deadline = Time.now + timeout_sec loop do return true if yield abort "FAIL timeout: #{label}" if Time.now >= deadline sleep 1 end end def owner_for(session_id) relay_redis.get(format(Streams::YoutubeRelay::OWNER_KEY, session_id)) end def relay_redis @relay_redis ||= Redis.new(url: ENV.fetch("REDIS_URL", "redis://redis:6379/0")) end