Initial commit: monorepo Match Live TV.
Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
89
backend/app/services/youtube/broadcast_service.rb
Normal file
89
backend/app/services/youtube/broadcast_service.rb
Normal file
@@ -0,0 +1,89 @@
|
||||
module Youtube
|
||||
class BroadcastService
|
||||
class Error < StandardError; end
|
||||
|
||||
def initialize(team)
|
||||
@team = team
|
||||
@credential = team.youtube_credential
|
||||
end
|
||||
|
||||
def create_broadcast!(title:, privacy_status: "unlisted", scheduled_at: nil)
|
||||
return mock_broadcast if @credential.blank? || missing_oauth_config?
|
||||
|
||||
client = authorized_client
|
||||
broadcast = Google::Apis::YoutubeV3::LiveBroadcast.new(
|
||||
snippet: Google::Apis::YoutubeV3::LiveBroadcastSnippet.new(
|
||||
title: title,
|
||||
scheduled_start_time: scheduled_at&.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_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, e.message
|
||||
end
|
||||
|
||||
def complete_broadcast!(broadcast_id)
|
||||
return if @credential.blank? || missing_oauth_config?
|
||||
|
||||
client = authorized_client
|
||||
client.transition_live_broadcast(broadcast_id, "complete")
|
||||
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
|
||||
end
|
||||
end
|
||||
25
backend/app/services/youtube/oauth_exchange.rb
Normal file
25
backend/app/services/youtube/oauth_exchange.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module Youtube
|
||||
class OauthExchange
|
||||
TOKEN_URL = "https://oauth2.googleapis.com/token".freeze
|
||||
|
||||
def self.call(code)
|
||||
response = Faraday.post(TOKEN_URL, {
|
||||
code: code,
|
||||
client_id: ENV["YOUTUBE_CLIENT_ID"],
|
||||
client_secret: ENV["YOUTUBE_CLIENT_SECRET"],
|
||||
redirect_uri: ENV["YOUTUBE_REDIRECT_URI"],
|
||||
grant_type: "authorization_code"
|
||||
})
|
||||
data = JSON.parse(response.body)
|
||||
raise BroadcastService::Error, data["error_description"] if data["error"]
|
||||
|
||||
{
|
||||
access_token: data["access_token"],
|
||||
refresh_token: data["refresh_token"],
|
||||
expires_in: data["expires_in"].to_i,
|
||||
channel_id: nil,
|
||||
channel_title: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
31
backend/app/services/youtube/oauth_refresh.rb
Normal file
31
backend/app/services/youtube/oauth_refresh.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Youtube
|
||||
class OauthRefresh
|
||||
TOKEN_URL = "https://oauth2.googleapis.com/token".freeze
|
||||
|
||||
def initialize(credential)
|
||||
@credential = credential
|
||||
end
|
||||
|
||||
def apply!(service)
|
||||
refresh! if @credential.expired?
|
||||
service.authorization = @credential.access_token
|
||||
service
|
||||
end
|
||||
|
||||
def refresh!
|
||||
response = Faraday.post(TOKEN_URL, {
|
||||
client_id: ENV["YOUTUBE_CLIENT_ID"],
|
||||
client_secret: ENV["YOUTUBE_CLIENT_SECRET"],
|
||||
refresh_token: @credential.refresh_token,
|
||||
grant_type: "refresh_token"
|
||||
})
|
||||
data = JSON.parse(response.body)
|
||||
raise Youtube::BroadcastService::Error, data["error"] if data["error"]
|
||||
|
||||
@credential.update!(
|
||||
access_token: data["access_token"],
|
||||
expires_at: Time.current + data["expires_in"].to_i.seconds
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
22
backend/app/services/youtube/oauth_url.rb
Normal file
22
backend/app/services/youtube/oauth_url.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module Youtube
|
||||
class OauthUrl
|
||||
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth".freeze
|
||||
SCOPES = [
|
||||
"https://www.googleapis.com/auth/youtube",
|
||||
"https://www.googleapis.com/auth/youtube.force-ssl"
|
||||
].join(" ").freeze
|
||||
|
||||
def self.build(team_id:, redirect_uri:)
|
||||
params = {
|
||||
client_id: ENV.fetch("YOUTUBE_CLIENT_ID", "not_configured"),
|
||||
redirect_uri: redirect_uri,
|
||||
response_type: "code",
|
||||
scope: SCOPES,
|
||||
access_type: "offline",
|
||||
prompt: "consent",
|
||||
state: team_id
|
||||
}
|
||||
"#{AUTH_URL}?#{URI.encode_www_form(params)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user