Files
MatchLiveTv/backend/app/services/youtube/broadcast_service.rb
Emiliano Frascaro 8263475146 Fix YouTube in attesa: audio AAC di riserva, retry attivazione broadcast
Il relay verso YouTube ora gestisce _air senza traccia audio, ritenta
activate_broadcast finché l'ingest è active e riduce la finestra
scheduledStartTime a ~20s.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 08:37:47 +02:00

157 lines
5.4 KiB
Ruby
Raw 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: true
)
)
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
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
def activate_broadcast!(broadcast_id)
return :skipped if broadcast_id.blank?
return :skipped if @credential.blank? || missing_oauth_config?
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"
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
return :waiting_ingest unless stream_status == "active"
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
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
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