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>
This commit is contained in:
@@ -1,13 +1,27 @@
|
|||||||
class YoutubeBroadcastActivateJob < ApplicationJob
|
class YoutubeBroadcastActivateJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(session_id)
|
MAX_ATTEMPTS = 36
|
||||||
|
RETRY_WAIT = 10.seconds
|
||||||
|
class << self
|
||||||
|
def schedule_with_retries(session_id, attempt: 1)
|
||||||
|
set(wait: attempt == 1 ? 8.seconds : RETRY_WAIT).perform_later(session_id, attempt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(session_id, attempt = 1)
|
||||||
session = StreamSession.find_by(id: session_id)
|
session = StreamSession.find_by(id: session_id)
|
||||||
return unless session&.platform == "youtube"
|
return unless session&.platform == "youtube"
|
||||||
return if session.terminal?
|
return if session.terminal?
|
||||||
return if session.youtube_broadcast_id.blank?
|
return if session.youtube_broadcast_id.blank?
|
||||||
return unless Streams::YoutubeRelay.running?(session.id)
|
return unless Streams::YoutubeRelay.running?(session.id)
|
||||||
|
|
||||||
Youtube::BroadcastService.new(session.match.team).activate_broadcast!(session.youtube_broadcast_id)
|
result = Youtube::BroadcastService.new(session.match.team).activate_broadcast!(session.youtube_broadcast_id)
|
||||||
|
return if result == :live || result == :transitioned
|
||||||
|
|
||||||
|
return unless result == :waiting_ingest
|
||||||
|
return if attempt >= MAX_ATTEMPTS
|
||||||
|
|
||||||
|
self.class.schedule_with_retries(session_id, attempt: attempt + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module Streams
|
|||||||
|
|
||||||
# YouTube richiede FLV H.264/AAC stabili; -c copy lascia spesso streamStatus=inactive.
|
# YouTube richiede FLV H.264/AAC stabili; -c copy lascia spesso streamStatus=inactive.
|
||||||
pid = Process.spawn(
|
pid = Process.spawn(
|
||||||
*youtube_ffmpeg_args(intake, output, log_path),
|
*youtube_ffmpeg_args(intake, output, log_path, with_audio: intake_has_audio?(intake)),
|
||||||
pgroup: true
|
pgroup: true
|
||||||
)
|
)
|
||||||
Process.detach(pid)
|
Process.detach(pid)
|
||||||
@@ -68,13 +68,28 @@ module Streams
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def youtube_ffmpeg_args(intake, output, log_path)
|
def youtube_ffmpeg_args(intake, output, log_path, with_audio:)
|
||||||
[
|
base = [
|
||||||
"ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "warning",
|
"ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "warning",
|
||||||
"-fflags", "+genpts+discardcorrupt", "-use_wallclock_as_timestamps", "1",
|
"-fflags", "+genpts+discardcorrupt", "-use_wallclock_as_timestamps", "1",
|
||||||
"-rw_timeout", "15000000",
|
"-rw_timeout", "15000000",
|
||||||
"-i", intake,
|
"-i", intake
|
||||||
"-map", "0:v:0", "-map", "0:a:0",
|
]
|
||||||
|
encode = youtube_encode_flags(output, log_path)
|
||||||
|
|
||||||
|
if with_audio
|
||||||
|
base + ["-map", "0:v:0", "-map", "0:a:0"] + encode
|
||||||
|
else
|
||||||
|
# _air senza audio (overlay 0:a?): YouTube richiede comunque AAC.
|
||||||
|
base + [
|
||||||
|
"-f", "lavfi", "-i", "anullsrc=channel_layout=mono:sample_rate=48000",
|
||||||
|
"-map", "0:v:0", "-map", "1:a:0"
|
||||||
|
] + encode
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def youtube_encode_flags(output, log_path)
|
||||||
|
[
|
||||||
"-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency",
|
"-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency",
|
||||||
"-pix_fmt", "yuv420p", "-r", "30", "-g", "30", "-keyint_min", "30", "-sc_threshold", "0",
|
"-pix_fmt", "yuv420p", "-r", "30", "-g", "30", "-keyint_min", "30", "-sc_threshold", "0",
|
||||||
"-b:v", "4000k", "-maxrate", "4000k", "-bufsize", "8000k",
|
"-b:v", "4000k", "-maxrate", "4000k", "-bufsize", "8000k",
|
||||||
@@ -84,10 +99,21 @@ module Streams
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def intake_has_audio?(url)
|
||||||
|
require "open3"
|
||||||
|
stdout, _stderr, status = Open3.capture3(
|
||||||
|
"ffprobe", "-v", "error", "-rw_timeout", "5000000",
|
||||||
|
"-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", url
|
||||||
|
)
|
||||||
|
status.success? && stdout.strip.present?
|
||||||
|
rescue StandardError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
def schedule_youtube_activate(session)
|
def schedule_youtube_activate(session)
|
||||||
return if session.youtube_broadcast_id.blank?
|
return if session.youtube_broadcast_id.blank?
|
||||||
|
|
||||||
YoutubeBroadcastActivateJob.set(wait: 8.seconds).perform_later(session.id)
|
YoutubeBroadcastActivateJob.schedule_with_retries(session.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mediamtx_intake_url(session)
|
def mediamtx_intake_url(session)
|
||||||
|
|||||||
@@ -51,31 +51,38 @@ module Youtube
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Porta in onda appena c’è ingest su YouTube (non attendere scheduledStartTime).
|
# Porta in onda appena c’è ingest su YouTube (non attendere scheduledStartTime).
|
||||||
|
# @return [Symbol] :live, :waiting_ingest, :transitioned, :skipped
|
||||||
def activate_broadcast!(broadcast_id)
|
def activate_broadcast!(broadcast_id)
|
||||||
return if broadcast_id.blank?
|
return :skipped if broadcast_id.blank?
|
||||||
return if @credential.blank? || missing_oauth_config?
|
return :skipped if @credential.blank? || missing_oauth_config?
|
||||||
|
|
||||||
client = authorized_client
|
client = authorized_client
|
||||||
item = client.list_live_broadcasts("status,contentDetails", id: broadcast_id).items&.first
|
item = client.list_live_broadcasts("status,contentDetails", id: broadcast_id).items&.first
|
||||||
return unless item
|
return :skipped unless item
|
||||||
|
|
||||||
status = item.status.life_cycle_status.to_s
|
status = item.status.life_cycle_status.to_s
|
||||||
return if status == "live"
|
return :live if status == "live"
|
||||||
|
|
||||||
stream_id = item.content_details&.bound_stream_id
|
stream_id = item.content_details&.bound_stream_id
|
||||||
if stream_id.present?
|
if stream_id.present?
|
||||||
stream = client.list_live_streams("status", id: stream_id).items&.first
|
stream = client.list_live_streams("status", id: stream_id).items&.first
|
||||||
return if stream&.status&.stream_status != "active"
|
stream_status = stream&.status&.stream_status.to_s
|
||||||
|
return :waiting_ingest unless stream_status == "active"
|
||||||
end
|
end
|
||||||
|
|
||||||
if %w[ready created].include?(status)
|
if %w[ready created].include?(status)
|
||||||
client.transition_live_broadcast("testing", broadcast_id, "id,status")
|
client.transition_live_broadcast("testing", broadcast_id, "id,status")
|
||||||
status = "testing"
|
status = "testing"
|
||||||
end
|
end
|
||||||
client.transition_live_broadcast("live", broadcast_id, "id,status") if status == "testing"
|
if status == "testing"
|
||||||
|
client.transition_live_broadcast("live", broadcast_id, "id,status")
|
||||||
|
return :transitioned
|
||||||
|
end
|
||||||
|
|
||||||
|
:skipped
|
||||||
rescue Google::Apis::Error => e
|
rescue Google::Apis::Error => e
|
||||||
Rails.logger.warn("[Youtube::BroadcastService] activate #{broadcast_id}: #{e.message}")
|
Rails.logger.warn("[Youtube::BroadcastService] activate #{broadcast_id}: #{e.message}")
|
||||||
nil
|
:skipped
|
||||||
end
|
end
|
||||||
|
|
||||||
def complete_broadcast!(broadcast_id)
|
def complete_broadcast!(broadcast_id)
|
||||||
@@ -131,7 +138,7 @@ module Youtube
|
|||||||
# YouTube richiede scheduledStartTime nel futuro; per «vai subito» usiamo ~1 min (auto_start al RTMP).
|
# 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 + 90.seconds
|
minimum = now + 20.seconds
|
||||||
maximum = now + 7.days
|
maximum = now + 7.days
|
||||||
|
|
||||||
candidate =
|
candidate =
|
||||||
|
|||||||
@@ -64,4 +64,5 @@ 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). |
|
| YouTube «tra X minuti» / COPERTINA | Relay ffmpeg verso YouTube non attivo o ingest `inactive`. Dopo deploy, riavvia la diretta dall’app; la live dovrebbe partire entro ~30s con segnale su `_air`. |
|
||||||
|
| YouTube resta in «waiting» | Verifica su server `log/youtube_relay_<session_id>.log` nel container **sidekiq**; attendi che `streamStatus` diventi `active` (il job ritenta l’attivazione per ~6 min). |
|
||||||
|
|||||||
Reference in New Issue
Block a user