Files
MatchLiveTv/backend/app/services/youtube/broadcast_service.rb
Emiliano Frascaro 854738b46d Stabilizza live YouTube/RTMP e fix crash rotazione mobile.
Pipeline YouTube con relay, overlay e publisher sync lato backend; fix GL pauseGlDuringRotation su Android per evitare crash in rotazione durante lo streaming Flutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 15:30:55 +02:00

171 lines
6.3 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
module Youtube
class BroadcastService
class Error < StandardError; end
def initialize(team, youtube_channel: nil)
@team = team
@resolver = CredentialResolver.new(team, channel: youtube_channel)
@credential = @resolver.resolve
end
def create_broadcast!(title:, privacy_status: "public", scheduled_at: nil)
return mock_broadcast if @credential.blank? || missing_oauth_config?
client = authorized_client
start_time = normalize_scheduled_start_time(scheduled_at)
broadcast = Google::Apis::YoutubeV3::LiveBroadcast.new(
snippet: Google::Apis::YoutubeV3::LiveBroadcastSnippet.new(
title: title,
scheduled_start_time: start_time.iso8601
),
status: Google::Apis::YoutubeV3::LiveBroadcastStatus.new(
privacy_status: privacy_status,
self_declared_made_for_kids: false
),
content_details: Google::Apis::YoutubeV3::LiveBroadcastContentDetails.new(
enable_auto_start: true,
enable_auto_stop: false
)
)
result = client.insert_live_broadcast("snippet,status,contentDetails", broadcast)
stream = Google::Apis::YoutubeV3::LiveStream.new(
snippet: Google::Apis::YoutubeV3::LiveStreamSnippet.new(title: "#{title} stream"),
cdn: Google::Apis::YoutubeV3::CdnSettings.new(
frame_rate: "30fps",
ingestion_type: "rtmp",
resolution: "720p"
)
)
stream_result = client.insert_live_stream("snippet,cdn", stream)
client.bind_live_broadcast(result.id, "id,contentDetails", stream_id: stream_result.id)
{
broadcast_id: result.id,
stream_id: stream_result.id,
stream_key: stream_result.cdn.ingestion_info.stream_name,
rtmp_url: stream_result.cdn.ingestion_info.ingestion_address
}
rescue Google::Apis::Error => e
ApiThrottle.mark_rate_limited! if ApiThrottle.rate_limit_error?(e.message)
raise Error, friendly_api_error(e)
end
# Porta in onda appena cè ingest su YouTube (non attendere scheduledStartTime).
# @return [Symbol] :live, :waiting_ingest, :transitioned, :skipped, :rate_limited
def activate_broadcast!(broadcast_id)
return :skipped if broadcast_id.blank?
return :skipped if @credential.blank? || missing_oauth_config?
return :rate_limited if ApiThrottle.rate_limited?
client = authorized_client
item = client.list_live_broadcasts("status,contentDetails", id: broadcast_id).items&.first
return :skipped unless item
status = item.status.life_cycle_status.to_s
return :live if status == "live"
return :broadcast_ended if status == "complete"
stream_id = item.content_details&.bound_stream_id
if stream_id.present?
stream = client.list_live_streams("status", id: stream_id).items&.first
stream_status = stream&.status&.stream_status.to_s
Rails.logger.info("[Youtube::BroadcastService] activate #{broadcast_id}: lifecycle=#{status} stream_status=#{stream_status.presence || "(blank)"}")
return :waiting_ingest unless stream_status == "active"
else
Rails.logger.info("[Youtube::BroadcastService] activate #{broadcast_id}: lifecycle=#{status} stream_status=(unbound)")
return :waiting_ingest
end
if %w[ready created].include?(status)
client.transition_live_broadcast("testing", broadcast_id, "id,status")
status = "testing"
end
if status == "testing"
client.transition_live_broadcast("live", broadcast_id, "id,status")
return :transitioned
end
:skipped
rescue Google::Apis::Error => e
if ApiThrottle.rate_limit_error?(e.message)
ApiThrottle.mark_rate_limited!
Rails.logger.warn("[Youtube::BroadcastService] activate rate limited #{broadcast_id}")
return :rate_limited
end
Rails.logger.warn("[Youtube::BroadcastService] activate #{broadcast_id}: #{e.message}")
:skipped
end
def complete_broadcast!(broadcast_id)
return if broadcast_id.blank?
return if @credential.blank? || missing_oauth_config?
client = authorized_client
client.transition_live_broadcast("complete", broadcast_id, "id,status")
rescue Google::Apis::Error
nil
end
def viewer_count(broadcast_id)
return 0 if @credential.blank? || missing_oauth_config?
client = authorized_client
resp = client.list_live_broadcasts("statistics", id: broadcast_id)
resp.items&.first&.statistics&.concurrent_viewers.to_i
rescue Google::Apis::Error
0
end
private
def mock_broadcast
{
broadcast_id: "mock_#{SecureRandom.hex(8)}",
stream_id: "mock_stream",
stream_key: ENV.fetch("YOUTUBE_MOCK_STREAM_KEY", "mock-stream-key"),
rtmp_url: "rtmp://a.rtmp.youtube.com/live2"
}
end
def missing_oauth_config?
ENV["YOUTUBE_CLIENT_ID"].blank?
end
def authorized_client
OauthRefresh.new(@credential).apply!(Google::Apis::YoutubeV3::YouTubeService.new)
end
def friendly_api_error(error)
msg = error.message.to_s
return "Il canale YouTube non ha la diretta attiva. Apri YouTube Studio con laccount del canale Match Live TV, abilita la live (verifica telefono se richiesto) e riprova." if msg.include?("liveStreamingNotEnabled")
return "Credenziali YouTube non valide. Ricollega il canale dalla sezione Admin → YouTube." if msg.match?(/Unauthorized|invalid_grant/i)
if msg.match?(/invalidScheduledStartTime|scheduledStartTimeRequired/i)
return "Orario programmazione YouTube non valido. Riprova: la diretta partirà appena invii il segnale RTMP."
end
return "YouTube temporaneamente occupato. Riprova tra 12 minuti." if ApiThrottle.rate_limit_error?(msg)
msg
end
# 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 + 20.seconds
maximum = now + 7.days
candidate =
if scheduled_at.blank?
minimum
else
scheduled_at.utc
end
candidate = minimum if candidate < minimum
candidate = maximum if candidate > maximum
candidate
end
end
end