diff --git a/backend/app/jobs/youtube_broadcast_setup_job.rb b/backend/app/jobs/youtube_broadcast_setup_job.rb new file mode 100644 index 0000000..ffa752e --- /dev/null +++ b/backend/app/jobs/youtube_broadcast_setup_job.rb @@ -0,0 +1,12 @@ +class YoutubeBroadcastSetupJob < ApplicationJob + queue_as :default + + retry_on Youtube::SetupBroadcast::Error, wait: 10.seconds, attempts: 3 + + def perform(session_id, youtube_channel = nil) + session = StreamSession.find_by(id: session_id) + return unless session + + Youtube::SetupBroadcast.call(session, youtube_channel: youtube_channel.presence) + end +end diff --git a/backend/app/services/sessions/create.rb b/backend/app/services/sessions/create.rb index 68eab97..74a86bc 100644 --- a/backend/app/services/sessions/create.rb +++ b/backend/app/services/sessions/create.rb @@ -27,10 +27,10 @@ module Sessions status: "idle" ) + youtube_channel = nil if session.platform == "youtube" youtube_channel = @params[:youtube_channel].presence assert_youtube_channel!(youtube_channel) - attach_youtube_broadcast!(session, youtube_channel: youtube_channel) end StreamSession.transaction do @@ -40,10 +40,13 @@ module Sessions mtx.create_path(session) mtx.create_overlay_path(session) start_overlay_relay(session) - start_youtube_relay(session) log_event(session, "pairing", { created: true, platform: session.platform }) end + if session.platform == "youtube" + YoutubeBroadcastSetupJob.perform_later(session.id, youtube_channel) + end + session end @@ -60,18 +63,7 @@ module Sessions end def attach_youtube_broadcast!(session, youtube_channel: nil) - yt = Youtube::BroadcastService.new(session.match.team, youtube_channel: youtube_channel) - title = "#{session.match.team.name} vs #{session.match.opponent_name}" - broadcast = yt.create_broadcast!( - title: title, - privacy_status: session.privacy_status, - scheduled_at: session.match.scheduled_at - ) - - session.youtube_broadcast_id = broadcast[:broadcast_id] - session.youtube_stream_id = broadcast[:stream_id] - session.stream_key = broadcast[:stream_key] - session.rtmp_url = broadcast[:rtmp_url] + Youtube::SetupBroadcast.call(session, youtube_channel: youtube_channel) end def start_overlay_relay(session) @@ -80,14 +72,6 @@ module Sessions Rails.logger.warn("[Sessions::Create] Overlay relay: #{e.message}") end - def start_youtube_relay(session) - return unless session.platform == "youtube" - - Streams::YoutubeRelay.start(session) - rescue Streams::YoutubeRelay::Error => e - Rails.logger.warn("[Sessions::Create] YouTube relay: #{e.message}") - end - def log_event(session, type, metadata) session.stream_events.create!(event_type: type, metadata: metadata, occurred_at: Time.current) end diff --git a/backend/app/services/youtube/setup_broadcast.rb b/backend/app/services/youtube/setup_broadcast.rb new file mode 100644 index 0000000..eed4e3c --- /dev/null +++ b/backend/app/services/youtube/setup_broadcast.rb @@ -0,0 +1,52 @@ +module Youtube + # Crea broadcast/stream YouTube e avvia relay (operazione lenta, ~20s). + class SetupBroadcast + class Error < StandardError; end + + def self.call(session, youtube_channel: nil) + new(session, youtube_channel: youtube_channel).call + end + + def initialize(session, youtube_channel: nil) + @session = session + @youtube_channel = youtube_channel + end + + def call + @session.reload + return @session if @session.terminal? + return @session if @session.platform != "youtube" + return @session if @session.youtube_broadcast_id.present? + + yt = BroadcastService.new(@session.match.team, channel: @youtube_channel) + title = "#{@session.match.team.name} vs #{@session.match.opponent_name}" + broadcast = yt.create_broadcast!( + title: title, + privacy_status: @session.privacy_status, + scheduled_at: @session.match.scheduled_at + ) + + @session.update!( + youtube_broadcast_id: broadcast[:broadcast_id], + youtube_stream_id: broadcast[:stream_id], + stream_key: broadcast[:stream_key], + rtmp_url: broadcast[:rtmp_url] + ) + + Streams::YoutubeRelay.start(@session) + @session.stream_events.create!( + event_type: "youtube_ready", + metadata: { broadcast_id: broadcast[:broadcast_id] }, + occurred_at: Time.current + ) + @session + rescue BroadcastService::Error => e + @session.stream_events.create!( + event_type: "error", + metadata: { source: "youtube_setup", message: e.message }, + occurred_at: Time.current + ) + raise Error, e.message + end + end +end diff --git a/mobile/lib/features/setup_wizard/step_transmission.dart b/mobile/lib/features/setup_wizard/step_transmission.dart index 8e05ffb..62cb96d 100644 --- a/mobile/lib/features/setup_wizard/step_transmission.dart +++ b/mobile/lib/features/setup_wizard/step_transmission.dart @@ -76,6 +76,17 @@ class _StepTransmissionState extends ConsumerState { message: apiErr!.message, billingUrl: apiErr.billingUrl, ); + } else if (e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.sendTimeout || + e.type == DioExceptionType.connectionTimeout) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Il server impiega più del previsto (YouTube). Attendi e non ritentare subito: ' + 'controlla se la sessione è già stata creata.', + ), + ), + ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(apiErr?.message ?? 'Errore nella creazione sessione')), diff --git a/mobile/lib/shared/api_client.dart b/mobile/lib/shared/api_client.dart index 4aa4479..8ade8f4 100644 --- a/mobile/lib/shared/api_client.dart +++ b/mobile/lib/shared/api_client.dart @@ -167,6 +167,10 @@ class ApiClient { int? targetBitrate, int? targetFps, }) async { + // YouTube: creazione broadcast + path MediaMTX può richiedere 20–30s lato server. + final sessionTimeout = platform == 'youtube' + ? const Duration(seconds: 60) + : const Duration(seconds: 30); final response = await _dio.post>( '/matches/$matchId/sessions', data: { @@ -177,6 +181,10 @@ class ApiClient { if (targetBitrate != null) 'target_bitrate': targetBitrate, if (targetFps != null) 'target_fps': targetFps, }, + options: Options( + sendTimeout: sessionTimeout, + receiveTimeout: sessionTimeout, + ), ); return StreamSession.fromJson(response.data!); }