Files
MatchLiveTv/backend/app/services/youtube/broadcast_service.rb
Emiliano Frascaro d08cd36527 Fix YouTube waiting: transcode verso RTMP e sync stato live.
Il relay non usa più copy (stream inactive su YouTube); PublisherSync
nel refresh overlay; activate solo quando lo stream YouTube è active.

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

150 lines
5.2 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).
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, mine: true).items&.first
return unless item
status = item.status.life_cycle_status.to_s
return if status == "live"
stream_id = item.content_details&.bound_stream_id
if stream_id.present?
stream = client.list_live_streams("status", id: stream_id, mine: true).items&.first
return if stream&.status&.stream_status != "active"
end
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?
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 + 90.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