diff --git a/backend/app/jobs/overlay_refresh_job.rb b/backend/app/jobs/overlay_refresh_job.rb index 18458f1..fbf0993 100644 --- a/backend/app/jobs/overlay_refresh_job.rb +++ b/backend/app/jobs/overlay_refresh_job.rb @@ -26,6 +26,9 @@ class OverlayRefreshJob < ApplicationJob return self.class.cancel_chain(session_id) unless Streams::OverlayRelay.running?(session_id) Streams::Overlay::Refresh.call(session) + if session.platform == "youtube" + Streams::YoutubeRelay.ensure_publishing!(session) + end self.class.set(wait: INTERVAL).perform_later(session_id) end end diff --git a/backend/app/jobs/youtube_broadcast_activate_job.rb b/backend/app/jobs/youtube_broadcast_activate_job.rb new file mode 100644 index 0000000..aeb3847 --- /dev/null +++ b/backend/app/jobs/youtube_broadcast_activate_job.rb @@ -0,0 +1,13 @@ +class YoutubeBroadcastActivateJob < ApplicationJob + queue_as :default + + def perform(session_id) + session = StreamSession.find_by(id: session_id) + return unless session&.platform == "youtube" + return if session.terminal? + return if session.youtube_broadcast_id.blank? + return unless Streams::YoutubeRelay.running?(session.id) + + Youtube::BroadcastService.new(session.match.team).activate_broadcast!(session.youtube_broadcast_id) + end +end diff --git a/backend/app/services/streams/youtube_relay.rb b/backend/app/services/streams/youtube_relay.rb index 4f838db..b1d363b 100644 --- a/backend/app/services/streams/youtube_relay.rb +++ b/backend/app/services/streams/youtube_relay.rb @@ -32,6 +32,7 @@ module Streams Process.detach(pid) store_pid(session.id, pid) Rails.logger.info("[YoutubeRelay] started pid=#{pid} session=#{session.id}") + schedule_youtube_activate(session) pid rescue Errno::ENOENT => e raise Error, "ffmpeg non disponibile: #{e.message}" @@ -68,6 +69,12 @@ module Streams private + def schedule_youtube_activate(session) + return if session.youtube_broadcast_id.blank? + + YoutubeBroadcastActivateJob.set(wait: 8.seconds).perform_later(session.id) + end + def mediamtx_intake_url(session) base = ENV.fetch("MEDIAMTX_INTERNAL_RTMP_URL", "rtmp://mediamtx:1935") "#{base.chomp('/')}/#{session.mediamtx_overlay_path_name}" diff --git a/backend/app/services/youtube/broadcast_service.rb b/backend/app/services/youtube/broadcast_service.rb index 19b096b..64a508c 100644 --- a/backend/app/services/youtube/broadcast_service.rb +++ b/backend/app/services/youtube/broadcast_service.rb @@ -50,6 +50,28 @@ module Youtube raise Error, friendly_api_error(e) end + # Porta in onda appena c’è ingest su YouTube (non attendere scheduledStartTime). + def activate_broadcast!(broadcast_id) + return if broadcast_id.blank? + return if @credential.blank? || missing_oauth_config? + + client = authorized_client + item = client.list_live_broadcasts("status", id: broadcast_id).items&.first + return unless item + + status = item.status.life_cycle_status.to_s + return if status == "live" + + if %w[ready created].include?(status) + client.transition_live_broadcast("testing", broadcast_id, "id,status") + status = "testing" + end + client.transition_live_broadcast("live", broadcast_id, "id,status") if status == "testing" + rescue Google::Apis::Error => e + Rails.logger.warn("[Youtube::BroadcastService] activate #{broadcast_id}: #{e.message}") + nil + end + def complete_broadcast!(broadcast_id) return if broadcast_id.blank? return if @credential.blank? || missing_oauth_config? @@ -100,10 +122,10 @@ module Youtube msg end - # YouTube richiede scheduledStartTime obbligatorio e nel futuro (non accetta date partita già passate). + # YouTube richiede scheduledStartTime nel futuro; per «vai subito» usiamo ~1 min (auto_start al RTMP). def normalize_scheduled_start_time(scheduled_at) now = Time.current.utc - minimum = now + 5.minutes + minimum = now + 90.seconds maximum = now + 7.days candidate = diff --git a/backend/spec/services/youtube/broadcast_service_spec.rb b/backend/spec/services/youtube/broadcast_service_spec.rb index f4cd1c5..9b725b4 100644 --- a/backend/spec/services/youtube/broadcast_service_spec.rb +++ b/backend/spec/services/youtube/broadcast_service_spec.rb @@ -4,10 +4,10 @@ RSpec.describe Youtube::BroadcastService do describe "#normalize_scheduled_start_time" do subject(:service) { described_class.allocate } - it "usa tra 5 minuti se scheduled_at è assente" do + it "usa tra ~90 secondi se scheduled_at è assente" do travel_to Time.zone.parse("2026-06-03 20:00:00 UTC") do t = service.send(:normalize_scheduled_start_time, nil) - expect(t).to eq(Time.utc(2026, 6, 3, 20, 5, 0)) + expect(t).to eq(Time.utc(2026, 6, 3, 20, 1, 30)) end end @@ -15,7 +15,7 @@ RSpec.describe Youtube::BroadcastService do travel_to Time.zone.parse("2026-06-03 20:00:00 UTC") do past = Time.zone.parse("2026-06-03 10:00:00") t = service.send(:normalize_scheduled_start_time, past) - expect(t).to eq(Time.utc(2026, 6, 3, 20, 5, 0)) + expect(t).to eq(Time.utc(2026, 6, 3, 20, 1, 30)) end end diff --git a/docs/APP_YOUTUBE_LIVE.md b/docs/APP_YOUTUBE_LIVE.md index e0e1af8..4d29945 100644 --- a/docs/APP_YOUTUBE_LIVE.md +++ b/docs/APP_YOUTUBE_LIVE.md @@ -64,3 +64,4 @@ Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmiss | YouTube non selezionabile | Token canale Match Live TV sul server (`admin` → YouTube piattaforma) | | OAuth «app non verificata» | Utente di test Google + Avanzate | | Live non su YouTube | Verifica relay ffmpeg / `stream_key` in sessione | +| YouTube «tra X minuti» | Il relay verso YouTube non era attivo; riavvia diretta dopo deploy. Con ingest attivo la live parte subito (non più +5 min). |