YouTube live immediata: relay sempre attivo e attivazione broadcast.
Il relay parte dal job overlay ogni 2s; scheduledStartTime ~90s invece di 5 min; transition testing/live quando l'ingest RTMP raggiunge YouTube. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,9 @@ class OverlayRefreshJob < ApplicationJob
|
|||||||
return self.class.cancel_chain(session_id) unless Streams::OverlayRelay.running?(session_id)
|
return self.class.cancel_chain(session_id) unless Streams::OverlayRelay.running?(session_id)
|
||||||
|
|
||||||
Streams::Overlay::Refresh.call(session)
|
Streams::Overlay::Refresh.call(session)
|
||||||
|
if session.platform == "youtube"
|
||||||
|
Streams::YoutubeRelay.ensure_publishing!(session)
|
||||||
|
end
|
||||||
self.class.set(wait: INTERVAL).perform_later(session_id)
|
self.class.set(wait: INTERVAL).perform_later(session_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
13
backend/app/jobs/youtube_broadcast_activate_job.rb
Normal file
13
backend/app/jobs/youtube_broadcast_activate_job.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class YoutubeBroadcastActivateJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(session_id)
|
||||||
|
session = StreamSession.find_by(id: session_id)
|
||||||
|
return unless session&.platform == "youtube"
|
||||||
|
return if session.terminal?
|
||||||
|
return if session.youtube_broadcast_id.blank?
|
||||||
|
return unless Streams::YoutubeRelay.running?(session.id)
|
||||||
|
|
||||||
|
Youtube::BroadcastService.new(session.match.team).activate_broadcast!(session.youtube_broadcast_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -32,6 +32,7 @@ module Streams
|
|||||||
Process.detach(pid)
|
Process.detach(pid)
|
||||||
store_pid(session.id, pid)
|
store_pid(session.id, pid)
|
||||||
Rails.logger.info("[YoutubeRelay] started pid=#{pid} session=#{session.id}")
|
Rails.logger.info("[YoutubeRelay] started pid=#{pid} session=#{session.id}")
|
||||||
|
schedule_youtube_activate(session)
|
||||||
pid
|
pid
|
||||||
rescue Errno::ENOENT => e
|
rescue Errno::ENOENT => e
|
||||||
raise Error, "ffmpeg non disponibile: #{e.message}"
|
raise Error, "ffmpeg non disponibile: #{e.message}"
|
||||||
@@ -68,6 +69,12 @@ module Streams
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def schedule_youtube_activate(session)
|
||||||
|
return if session.youtube_broadcast_id.blank?
|
||||||
|
|
||||||
|
YoutubeBroadcastActivateJob.set(wait: 8.seconds).perform_later(session.id)
|
||||||
|
end
|
||||||
|
|
||||||
def mediamtx_intake_url(session)
|
def mediamtx_intake_url(session)
|
||||||
base = ENV.fetch("MEDIAMTX_INTERNAL_RTMP_URL", "rtmp://mediamtx:1935")
|
base = ENV.fetch("MEDIAMTX_INTERNAL_RTMP_URL", "rtmp://mediamtx:1935")
|
||||||
"#{base.chomp('/')}/#{session.mediamtx_overlay_path_name}"
|
"#{base.chomp('/')}/#{session.mediamtx_overlay_path_name}"
|
||||||
|
|||||||
@@ -50,6 +50,28 @@ module Youtube
|
|||||||
raise Error, friendly_api_error(e)
|
raise Error, friendly_api_error(e)
|
||||||
end
|
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).items&.first
|
||||||
|
return unless item
|
||||||
|
|
||||||
|
status = item.status.life_cycle_status.to_s
|
||||||
|
return if status == "live"
|
||||||
|
|
||||||
|
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)
|
def complete_broadcast!(broadcast_id)
|
||||||
return if broadcast_id.blank?
|
return if broadcast_id.blank?
|
||||||
return if @credential.blank? || missing_oauth_config?
|
return if @credential.blank? || missing_oauth_config?
|
||||||
@@ -100,10 +122,10 @@ module Youtube
|
|||||||
msg
|
msg
|
||||||
end
|
end
|
||||||
|
|
||||||
# YouTube richiede scheduledStartTime obbligatorio e nel futuro (non accetta date partita già passate).
|
# YouTube richiede scheduledStartTime nel futuro; per «vai subito» usiamo ~1 min (auto_start al RTMP).
|
||||||
def normalize_scheduled_start_time(scheduled_at)
|
def normalize_scheduled_start_time(scheduled_at)
|
||||||
now = Time.current.utc
|
now = Time.current.utc
|
||||||
minimum = now + 5.minutes
|
minimum = now + 90.seconds
|
||||||
maximum = now + 7.days
|
maximum = now + 7.days
|
||||||
|
|
||||||
candidate =
|
candidate =
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ RSpec.describe Youtube::BroadcastService do
|
|||||||
describe "#normalize_scheduled_start_time" do
|
describe "#normalize_scheduled_start_time" do
|
||||||
subject(:service) { described_class.allocate }
|
subject(:service) { described_class.allocate }
|
||||||
|
|
||||||
it "usa tra 5 minuti se scheduled_at è assente" do
|
it "usa tra ~90 secondi se scheduled_at è assente" do
|
||||||
travel_to Time.zone.parse("2026-06-03 20:00:00 UTC") do
|
travel_to Time.zone.parse("2026-06-03 20:00:00 UTC") do
|
||||||
t = service.send(:normalize_scheduled_start_time, nil)
|
t = service.send(:normalize_scheduled_start_time, nil)
|
||||||
expect(t).to eq(Time.utc(2026, 6, 3, 20, 5, 0))
|
expect(t).to eq(Time.utc(2026, 6, 3, 20, 1, 30))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ RSpec.describe Youtube::BroadcastService do
|
|||||||
travel_to Time.zone.parse("2026-06-03 20:00:00 UTC") do
|
travel_to Time.zone.parse("2026-06-03 20:00:00 UTC") do
|
||||||
past = Time.zone.parse("2026-06-03 10:00:00")
|
past = Time.zone.parse("2026-06-03 10:00:00")
|
||||||
t = service.send(:normalize_scheduled_start_time, past)
|
t = service.send(:normalize_scheduled_start_time, past)
|
||||||
expect(t).to eq(Time.utc(2026, 6, 3, 20, 5, 0))
|
expect(t).to eq(Time.utc(2026, 6, 3, 20, 1, 30))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -64,3 +64,4 @@ Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmiss
|
|||||||
| YouTube non selezionabile | Token canale Match Live TV sul server (`admin` → YouTube piattaforma) |
|
| YouTube non selezionabile | Token canale Match Live TV sul server (`admin` → YouTube piattaforma) |
|
||||||
| OAuth «app non verificata» | Utente di test Google + Avanzate |
|
| OAuth «app non verificata» | Utente di test Google + Avanzate |
|
||||||
| Live non su YouTube | Verifica relay ffmpeg / `stream_key` in sessione |
|
| Live non su YouTube | Verifica relay ffmpeg / `stream_key` in sessione |
|
||||||
|
| YouTube «tra X minuti» | Il relay verso YouTube non era attivo; riavvia diretta dopo deploy. Con ingest attivo la live parte subito (non più +5 min). |
|
||||||
|
|||||||
Reference in New Issue
Block a user