diff --git a/backend/app/controllers/concerns/mediamtx_playback.rb b/backend/app/controllers/concerns/mediamtx_playback.rb index 330de04..4d205f1 100644 --- a/backend/app/controllers/concerns/mediamtx_playback.rb +++ b/backend/app/controllers/concerns/mediamtx_playback.rb @@ -4,11 +4,20 @@ module MediamtxPlayback private def mediamtx_paths_index - @mediamtx_paths_index ||= Mediamtx::Client.new.list_paths.index_by { |i| i["name"] } + mediamtx_paths_index_for_url(MatchLiveTv.mediamtx_api_url) + end + + def mediamtx_paths_index_for(session) + mediamtx_paths_index_for_url(session.mediamtx_api_base_url) + end + + def mediamtx_paths_index_for_url(api_url) + @mediamtx_paths_by_origin ||= {} + @mediamtx_paths_by_origin[api_url] ||= Mediamtx::Client.new(base_url: api_url).list_paths.index_by { |i| i["name"] } end def mediamtx_path_info(session, path_name: mediamtx_playback_path_name(session)) - mediamtx_paths_index[path_name] + mediamtx_paths_index_for(session)[path_name] end def mediamtx_playback_path_name(session) @@ -28,7 +37,7 @@ module MediamtxPlayback end def mediamtx_publisher_online?(session) - info = mediamtx_paths_index[session.mediamtx_path_name] + info = mediamtx_paths_index_for(session)[session.mediamtx_path_name] info && info["online"] end end diff --git a/backend/app/controllers/hls_proxy_controller.rb b/backend/app/controllers/hls_proxy_controller.rb index e877057..8a44dbe 100644 --- a/backend/app/controllers/hls_proxy_controller.rb +++ b/backend/app/controllers/hls_proxy_controller.rb @@ -20,7 +20,8 @@ class HlsProxyController < ActionController::Base def proxy_mediamtx_path(upstream_path) return head :not_found if upstream_path.blank? - upstream = "#{MatchLiveTv.mediamtx_hls_url}/#{upstream_path}" + origin = hls_origin_for(upstream_path) + upstream = "#{origin}/#{upstream_path}" upstream = "#{upstream}?#{request.query_string}" if request.query_string.present? cookie = request.headers["Cookie"].presence || "cookieCheck=1" @@ -30,7 +31,7 @@ class HlsProxyController < ActionController::Base if response.status.in?([301, 302, 307, 308]) location = response.headers["location"].to_s cookie = cookie_from_set_header(response.headers["set-cookie"]).presence || cookie - upstream = resolve_upstream_url(location) + upstream = resolve_upstream_url(location, origin) next end @@ -43,10 +44,21 @@ class HlsProxyController < ActionController::Base head :bad_gateway end - def resolve_upstream_url(location) + def hls_origin_for(upstream_path) + session_id = upstream_path[/\Alive\/match_([0-9a-f-]{36})/i, 1] + if session_id + session = StreamSession.find_by(id: session_id) + node_hls = session&.stream_node&.internal_hls_url.presence + return node_hls.sub(%r{/$}, "") if node_hls.present? + end + + MatchLiveTv.mediamtx_hls_url.sub(%r{/$}, "") + end + + def resolve_upstream_url(location, origin) return location if location.start_with?("http://", "https://") - base = MatchLiveTv.mediamtx_hls_url.sub(%r{/$}, "") + base = origin.to_s.sub(%r{/$}, "") location.start_with?("/") ? "#{base}#{location}" : "#{base}/#{location}" end diff --git a/backend/app/jobs/youtube_relay_ensure_job.rb b/backend/app/jobs/youtube_relay_ensure_job.rb index bf75674..2c36f57 100644 --- a/backend/app/jobs/youtube_relay_ensure_job.rb +++ b/backend/app/jobs/youtube_relay_ensure_job.rb @@ -1,4 +1,4 @@ -# Avvia/riavvia il relay YouTube solo sui worker con YOUTUBE_RELAY_WORKER=1 (coda youtube_relay). +# Avvia/riavvia il relay YouTube sul worker del nodo (coda youtube_relay_). class YoutubeRelayEnsureJob < ApplicationJob queue_as Streams::YoutubeRelay::QUEUE diff --git a/backend/app/jobs/youtube_relay_stop_job.rb b/backend/app/jobs/youtube_relay_stop_job.rb index cf868f3..2b366a4 100644 --- a/backend/app/jobs/youtube_relay_stop_job.rb +++ b/backend/app/jobs/youtube_relay_stop_job.rb @@ -17,6 +17,7 @@ class YoutubeRelayStopJob < ApplicationJob return unless result == :wrong_host return if attempts >= 30 - self.class.set(wait: 2.seconds).perform_later(session_id, attempts + 1) + self.class.set(wait: 2.seconds, queue: Streams::YoutubeRelay.queue_for(session)) + .perform_later(session_id, attempts + 1) end end diff --git a/backend/app/models/youtube_credential.rb b/backend/app/models/youtube_credential.rb index 808a54b..5bd1335 100644 --- a/backend/app/models/youtube_credential.rb +++ b/backend/app/models/youtube_credential.rb @@ -4,19 +4,33 @@ class YoutubeCredential < ApplicationRecord attr_encrypted :access_token, key: :encryption_key, attribute: "access_token_encrypted", - mode: :single_iv_salt + mode: :single_iv_and_salt, + algorithm: "aes-256-cbc", + iv: :encryption_iv attr_encrypted :refresh_token, key: :encryption_key, attribute: "refresh_token_encrypted", - mode: :single_iv_salt + mode: :single_iv_and_salt, + algorithm: "aes-256-cbc", + iv: :encryption_iv def expired? expires_at.present? && expires_at < Time.current end + def usable? + refresh_token.present? + rescue ArgumentError, OpenSSL::Cipher::CipherError, NoMethodError + false + end + private def encryption_key Rails.application.secret_key_base[0, 32] end + + def encryption_iv + encryption_key[0, 16] + end end diff --git a/backend/app/services/mediamtx/client.rb b/backend/app/services/mediamtx/client.rb index 92a5ab0..4388b55 100644 --- a/backend/app/services/mediamtx/client.rb +++ b/backend/app/services/mediamtx/client.rb @@ -22,8 +22,7 @@ module Mediamtx def create_path(session) path = session.mediamtx_path_name # record: false finché non c'è publisher — con alwaysAvailable MediaMTX registrerebbe - # solo la slate (schermo nero) in pausa/attesa. - # YouTube: niente slate sul path camera (maschera il video al relay ffmpeg). + # solo la slate in pausa/attesa. Slate resta accesa anche su YouTube (copertina se l'app cade). body = recording_body(session, enabled: false).merge( source: "publisher", overridePublisher: true @@ -76,7 +75,7 @@ module Mediamtx end # Slate alwaysAvailable: copertina sullo stesso path quando il telefono è offline. - # Disattivare quando il publisher è in onda; riattivare in pausa/disconnessione. + # Resta accesa anche con publisher in onda (MediaMTX usa il publisher se presente). def set_always_available(session, enabled:) path = session.mediamtx_path_name return true if always_available_remembered?(path, enabled: enabled) diff --git a/backend/app/services/mediamtx/publisher_sync.rb b/backend/app/services/mediamtx/publisher_sync.rb index 39340db..157fd19 100644 --- a/backend/app/services/mediamtx/publisher_sync.rb +++ b/backend/app/services/mediamtx/publisher_sync.rb @@ -19,7 +19,6 @@ module Mediamtx if @session.paused? # RTMP ancora connesso in pausa: non forzare live/reconnect. else - enable_live_path_once!(@session) if @session.may_go_live? @session.go_live! @session.reload @@ -75,23 +74,10 @@ module Mediamtx @session.update!(timeout_job_id: job) end - # Compatibilità con webhook / controller. def self.schedule_youtube_pipeline!(session, force: false) Youtube::LivePipeline.schedule!(session, force: force) end - def enable_live_path_once!(session) - return unless session.platform == "youtube" - - key = format("youtube:slate_disabled:%s", session.id) - return unless redis.set(key, "1", nx: true, ex: 48.hours.to_i) - - Client.for_session(session).set_always_available(session, enabled: false) - rescue Client::Error => e - redis.del(format("youtube:slate_disabled:%s", session.id)) - Rails.logger.warn("[PublisherSync] disable slate session=#{session.id}: #{e.message}") - end - def restore_slate_path!(session) return if session.platform == "matchlivetv" @@ -183,7 +169,7 @@ module Mediamtx key = format("youtube_relay:sched:%s", session.id) return unless redis.set(key, "1", nx: true, ex: 10) - YoutubeRelayEnsureJob.set(queue: Streams::YoutubeRelay::QUEUE).perform_later(session.id) + Streams::YoutubeRelay.enqueue_ensure!(session) end def redis diff --git a/backend/app/services/streams/node_provisioner.rb b/backend/app/services/streams/node_provisioner.rb index 92c9060..2062d4f 100644 --- a/backend/app/services/streams/node_provisioner.rb +++ b/backend/app/services/streams/node_provisioner.rb @@ -77,9 +77,8 @@ module Streams dns.upsert_a(hostname, ip) simulated = instance.raw.is_a?(Hash) && (instance.raw[:simulated] || instance.raw["simulated"]) - api_base = simulated ? home.api_base_url : "http://#{private_ip}:9997" - internal_rtmp = simulated ? home.internal_rtmp_url : "rtmp://#{private_ip}:1935" - internal_hls = simulated ? home.internal_hls_url : "http://#{private_ip}:8888" + urls = urls_for_node(role: role, home: home, hostname: hostname, simulated: simulated, + private_ip: private_ip, public_ip: ip, use_node_hostname: use_node_hostname) StreamNode.create!( slug: slug, @@ -88,11 +87,11 @@ module Streams status: "ready", provider: provider_name_for(cloud, role: role), provider_instance_id: instance.id, - rtmp_base_url: use_node_hostname ? "rtmp://#{hostname}:1935" : home.rtmp_base_url, - hls_base_url: use_node_hostname ? "https://#{hostname}/hls" : home.hls_base_url, - api_base_url: api_base, - internal_rtmp_url: internal_rtmp, - internal_hls_url: internal_hls, + rtmp_base_url: urls.fetch(:rtmp_base_url), + hls_base_url: urls.fetch(:hls_base_url), + api_base_url: urls.fetch(:api_base_url), + internal_rtmp_url: urls.fetch(:internal_rtmp_url), + internal_hls_url: urls.fetch(:internal_hls_url), max_publishers: max, max_relays: max, last_health_at: Time.current, @@ -105,6 +104,39 @@ module Streams ) end + def urls_for_node(role:, home:, hostname:, simulated:, private_ip:, public_ip: nil, use_node_hostname:) + if role == "lab" && ENV["MEDIAMTX_LAB_API_URL"].present? + return { + api_base_url: ENV.fetch("MEDIAMTX_LAB_API_URL"), + internal_rtmp_url: ENV.fetch("MEDIAMTX_LAB_INTERNAL_RTMP_URL", "rtmp://mediamtx_lab:1935"), + internal_hls_url: ENV.fetch("MEDIAMTX_LAB_HLS_URL", "http://mediamtx_lab:8888"), + rtmp_base_url: ENV.fetch("MEDIAMTX_LAB_RTMP_URL", "rtmp://127.0.0.1:11935"), + # HLS pubblico resta sul proxy del sito: il controller instrada al MediaMTX del nodo. + hls_base_url: home.hls_base_url + } + end + + control_ip = control_ip_for(role: role, private_ip: private_ip, public_ip: public_ip) + { + api_base_url: simulated ? home.api_base_url : "http://#{control_ip}:9997", + internal_rtmp_url: simulated ? home.internal_rtmp_url : "rtmp://#{control_ip}:1935", + internal_hls_url: simulated ? home.internal_hls_url : "http://#{control_ip}:8888", + rtmp_base_url: use_node_hostname ? "rtmp://#{hostname}:1935" : home.rtmp_base_url, + # Player del sito: proxy Rails/edge. HTTPS diretto sul nodo arriva dopo TLS/Caddy. + hls_base_url: home.hls_base_url + } + end + + # Senza WireGuard/network privata Rails deve parlare con l'IPv4 pubblico (API+HLS). + def control_ip_for(role:, private_ip:, public_ip:) + public_ip = public_ip.presence + return private_ip if role != "cloud" || public_ip.blank? + return public_ip if ENV["STREAM_CLOUD_PUBLIC_CONTROL"] == "1" + return public_ip if private_ip.blank? || private_ip == public_ip + + private_ip + end + def next_slug(prefix) used = StreamNode.where("slug LIKE ?", "#{prefix}-%").pluck(:slug) n = 1 diff --git a/backend/app/services/streams/youtube_relay.rb b/backend/app/services/streams/youtube_relay.rb index eb9677d..51e4c6e 100644 --- a/backend/app/services/streams/youtube_relay.rb +++ b/backend/app/services/streams/youtube_relay.rb @@ -1,6 +1,10 @@ +require "json" +require "net/http" +require "uri" + module Streams - # Relay verso YouTube: legge RTMP/HLS da MediaMTX e inoltra su RTMPS (-c copy). Nessun overlay. - # ffmpeg gira solo sui worker Sidekiq con YOUTUBE_RELAY_WORKER=1 (coda youtube_relay). + # Relay verso YouTube: legge HLS da MediaMTX e inoltra su RTMPS (-c copy). + # Sul nodo cloud ffmpeg è avviato dall'agent locale (immagine CPX) all'avvio diretta. class YoutubeRelay class Error < StandardError; end @@ -8,6 +12,7 @@ module Streams OWNER_KEY = "youtube_relay:owner:%s" OWNED_SET = "youtube_relay:owned:%s" QUEUE = :youtube_relay + QUEUE_PREFIX = "youtube_relay" class << self def worker? @@ -18,18 +23,44 @@ module Streams ENV.fetch("RELAY_MAX_CONCURRENT", "4").to_i end + def local_node_slug + ENV["STREAM_NODE_SLUG"].presence || NodeRegistry::HOME_SLUG + end + + def assigned_node_slug(session) + session.stream_node&.slug.presence || NodeRegistry::HOME_SLUG + end + + def node_matches?(session) + assigned_node_slug(session) == local_node_slug + end + + def queue_for(session) + "#{QUEUE_PREFIX}_#{assigned_node_slug(session)}" + end + + def enqueue_ensure!(session, wait: nil) + opts = { queue: queue_for(session) } + opts[:wait] = wait if wait + YoutubeRelayEnsureJob.set(**opts).perform_later(session.id) + end + + def enqueue_stop!(session) + YoutubeRelayStopJob.set(queue: queue_for(session)).perform_later(session.id) + end + def start(session) - return unless worker? + return enqueue_ensure!(session) unless worker? && node_matches?(session) start_on_worker!(session) end # Non cancella owner/pid qui: solo il worker owner deve killare ffmpeg. def stop(session) - if worker? && owner_is_local?(session.id) + if worker? && owner_is_local?(session.id) && node_matches?(session) stop_on_worker!(session) else - YoutubeRelayStopJob.set(queue: QUEUE).perform_later(session.id) + enqueue_stop!(session) end true end @@ -41,7 +72,7 @@ module Streams return false if pid.blank? && owner.present? && owner != worker_id && redis.ttl(format(OWNER_KEY, session_id)) <= 30 return false if pid.blank? - return process_alive?(pid) if owner.blank? || owner == worker_id + return process_alive?(pid, session_id: session_id) if owner.blank? || owner == worker_id # Relay su altro host: attivo se lock owner ancora fresco. redis.ttl(format(OWNER_KEY, session_id)) > 30 @@ -52,15 +83,22 @@ module Streams return if session.terminal? return if session.stream_key.blank? - if worker? + if worker? && node_matches?(session) ensure_on_worker!(session) else - YoutubeRelayEnsureJob.set(queue: QUEUE).perform_later(session.id) + enqueue_ensure!(session) end end def ensure_on_worker!(session) return :not_worker unless worker? + unless node_matches?(session) + enqueue_ensure!(session, wait: 2.seconds) + Rails.logger.info( + "[YoutubeRelay] wrong_node local=#{local_node_slug} assigned=#{assigned_node_slug(session)} session=#{session.id}" + ) + return :wrong_host + end return unless session.platform == "youtube" return if session.terminal? return if session.stream_key.blank? @@ -68,7 +106,7 @@ module Streams return unless intake_available?(session) pid = pid_for(session.id) - if pid.present? && !process_alive?(pid.to_i) + if pid.present? && !process_alive?(pid.to_i, session_id: session.id) clear_local_ownership(session.id) end @@ -78,8 +116,8 @@ module Streams end if at_capacity? - YoutubeRelayEnsureJob.set(wait: 5.seconds, queue: QUEUE).perform_later(session.id) - Rails.logger.info("[YoutubeRelay] at capacity worker=#{worker_id} session=#{session.id} requeue") + enqueue_ensure!(session, wait: 5.seconds) + Rails.logger.info("[YoutubeRelay] at capacity worker=#{worker_id} node=#{local_node_slug} session=#{session.id} requeue") return :at_capacity end @@ -109,7 +147,7 @@ module Streams return :noop end - terminate_pid(pid) + terminate_pid(pid, session_id: session.id) clear_local_ownership(session.id) Rails.logger.info("[YoutubeRelay] stopped pid=#{pid} session=#{session.id} worker=#{worker_id}") :stopped @@ -140,26 +178,88 @@ module Streams stop_on_worker!(session) if pid_for(session.id).present? && owner_is_local?(session.id) + # Due EnsureJob concorrenti (concurrency Sidekiq > 1) non devono spawnare due ffmpeg. + unless redis.set(format("youtube_relay:startlock:%s", session.id), worker_id, nx: true, ex: 20) + return pid_for(session.id).to_i + end + log_path = log_file(session) FileUtils.mkdir_p(File.dirname(log_path)) intake_source = mediamtx_intake_source(session) output = "rtmps://a.rtmps.youtube.com/live2/#{session.stream_key}" - pid = Process.spawn( - *youtube_ffmpeg_args(intake_source, output), - %i[out err] => log_path, - pgroup: true - ) - Process.detach(pid) + pid = if relay_agent_enabled? + start_via_agent!(session) + else + spawn_ffmpeg!(youtube_ffmpeg_args(intake_source, output), log_path) + end store_pid(session.id, pid) claim_ownership!(session.id) - Rails.logger.info("[YoutubeRelay] started pid=#{pid} session=#{session.id} intake=#{intake_source.join(":")} worker=#{worker_id}") + Rails.logger.info( + "[YoutubeRelay] started pid=#{pid} session=#{session.id} intake=#{intake_source.join(':')} " \ + "worker=#{worker_id} agent=#{relay_agent_url || '-'}" + ) schedule_youtube_activate(session) pid rescue Errno::ENOENT => e raise Error, "ffmpeg non disponibile: #{e.message}" end + def relay_agent_url + ENV["STREAM_NODE_RELAY_AGENT_URL"].presence + end + + def relay_agent_enabled? + relay_agent_url.present? + end + + def relay_agent_secret + ENV["STREAM_NODE_AGENT_SECRET"].presence || ENV["MEDIAMTX_WEBHOOK_SECRET"].presence || "" + end + + def spawn_ffmpeg!(args, log_path) + pid = Process.spawn(*args, %i[out err] => log_path, pgroup: true) + Process.detach(pid) + pid + end + + def start_via_agent!(session) + payload = { + "session_id" => session.id, + "path" => session.mediamtx_path_name, + "rtmps" => "rtmps://a.rtmps.youtube.com/live2/#{session.stream_key}" + } + res = agent_request(Net::HTTP::Post, "/relays", payload) + pid = res["pid"].to_i + raise Error, "relay agent spawn failed: #{res.inspect}" if pid <= 0 + + File.write( + log_file(session), + "agent=#{relay_agent_url} pid=#{pid} path=#{session.mediamtx_path_name}\n" + ) + pid + end + + def agent_request(http_class, path, payload = nil) + uri = URI.parse("#{relay_agent_url.chomp('/')}#{path}") + http = Net::HTTP.new(uri.host, uri.port) + http.open_timeout = 5 + http.read_timeout = 10 + req = http_class.new(uri) + req["Authorization"] = "Bearer #{relay_agent_secret}" if relay_agent_secret.present? + req["Content-Type"] = "application/json" + req.body = JSON.generate(payload) if payload + res = http.request(req) + body = res.body.present? ? JSON.parse(res.body) : {} + unless res.is_a?(Net::HTTPSuccess) + raise Error, "relay agent #{path} HTTP #{res.code} #{body.inspect}" + end + + body + rescue JSON::ParserError => e + raise Error, "relay agent JSON: #{e.message}" + end + # Remux verso YouTube: video+audio copy (AAC già da app/slate a 48k mono). # HLS (ADTS) → FLV richiede -bsf:a aac_adtstoasc; RTMP ha già ASC in FLV tags. def youtube_ffmpeg_args(intake_source, output) @@ -179,6 +279,9 @@ module Streams end common_head + [ + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "2", "-rw_timeout", "15000000", "-live_start_index", "-1", "-i", url, @@ -190,14 +293,29 @@ module Streams end def mediamtx_intake_source(session) - base = session.mediamtx_internal_rtmp_url + rtmp_base, hls_base = intake_bases(session) if Mediamtx::PublisherOnline.active?(session) - return [:rtmp, "#{base.chomp('/')}/#{session.mediamtx_path_name}"] + return [:rtmp, "#{rtmp_base.chomp('/')}/#{session.mediamtx_path_name}"] end path = session.mediamtx_path_name - hls = session.mediamtx_internal_hls_url.chomp("/") - [:hls, "#{hls}/#{path}/index.m3u8"] + [:hls, "#{hls_base.chomp('/')}/#{path}/index.m3u8"] + end + + # Sul nodo assegnato ffmpeg legge MediaMTX in loopback (o hostname Docker locale). + def intake_bases(session) + if relay_agent_enabled? && node_matches?(session) + return ["rtmp://127.0.0.1:1935", "http://127.0.0.1:8888"] + end + + if node_matches?(session) + [ + ENV["STREAM_NODE_LOCAL_RTMP_URL"].presence || session.mediamtx_internal_rtmp_url, + ENV["STREAM_NODE_LOCAL_HLS_URL"].presence || session.mediamtx_internal_hls_url + ] + else + [session.mediamtx_internal_rtmp_url, session.mediamtx_internal_hls_url] + end end def intake_available?(session) @@ -257,7 +375,9 @@ module Streams redis.get(format(REDIS_KEY, session_id)) end - def process_alive?(pid) + def process_alive?(pid, session_id: nil) + return agent_session_running?(session_id) if relay_agent_enabled? && session_id.present? + stat = File.read("/proc/#{pid.to_i}/stat") return false if stat.split[2] == "Z" @@ -267,12 +387,26 @@ module Streams false end - def terminate_pid(pid) + def agent_session_running?(session_id) + res = agent_request(Net::HTTP::Get, "/relays/#{session_id}") + res["running"] == true + rescue Error + false + end + + def terminate_pid(pid, session_id: nil) + if relay_agent_enabled? && session_id.present? + agent_request(Net::HTTP::Delete, "/relays/#{session_id}") + return + end + Process.kill("TERM", -pid.to_i) sleep 0.5 rescue Errno::ESRCH nil else + return if relay_agent_enabled? + begin Process.kill("KILL", -pid.to_i) rescue Errno::ESRCH diff --git a/backend/app/services/youtube/credential_resolver.rb b/backend/app/services/youtube/credential_resolver.rb index c01aead..f4ff451 100644 --- a/backend/app/services/youtube/credential_resolver.rb +++ b/backend/app/services/youtube/credential_resolver.rb @@ -32,7 +32,8 @@ module Youtube end def team_channel_available? - @ent.youtube_enabled? && @ent.premium_full? && @mode == "team" && @team.club.youtube_credential.present? + @ent.youtube_enabled? && @ent.premium_full? && @mode == "team" && + @team.club.youtube_credential&.usable? end def effective_channel diff --git a/backend/app/services/youtube/live_pipeline.rb b/backend/app/services/youtube/live_pipeline.rb index d323eb7..0742fd3 100644 --- a/backend/app/services/youtube/live_pipeline.rb +++ b/backend/app/services/youtube/live_pipeline.rb @@ -63,7 +63,6 @@ module Youtube return end - Mediamtx::Client.for_session(session).set_always_available(session, enabled: false) session.go_live! if session.may_go_live? session.reconnect! if session.reconnecting? && session.may_reconnect? diff --git a/backend/app/services/youtube/team_status.rb b/backend/app/services/youtube/team_status.rb index 15ebb12..352c55f 100644 --- a/backend/app/services/youtube/team_status.rb +++ b/backend/app/services/youtube/team_status.rb @@ -15,7 +15,7 @@ module Youtube when "matchlivetv_light" PlatformCredential.configured? when "team" - @team.club.youtube_credential.present? || PlatformCredential.configured? + @team.club.youtube_credential&.usable? || PlatformCredential.configured? else false end @@ -31,7 +31,7 @@ module Youtube if @mode == "matchlivetv_light" PlatformCredential.configured? elsif @mode == "team" - @team.club.youtube_credential.blank? && PlatformCredential.configured? + !@team.club.youtube_credential&.usable? && PlatformCredential.configured? else false end @@ -46,7 +46,7 @@ module Youtube end def needs_team_oauth? - @mode == "team" && @ent.premium_full? && @team.club.youtube_credential.blank? && !PlatformCredential.configured? + @mode == "team" && @ent.premium_full? && !@team.club.youtube_credential&.usable? && !PlatformCredential.configured? end end end diff --git a/backend/app/views/public/live/show.html.erb b/backend/app/views/public/live/show.html.erb index 5b4bf6d..a2d3134 100644 --- a/backend/app/views/public/live/show.html.erb +++ b/backend/app/views/public/live/show.html.erb @@ -54,6 +54,7 @@ +