Stabilizza live YouTube/RTMP e fix crash rotazione mobile.
Pipeline YouTube con relay, overlay e publisher sync lato backend; fix GL pauseGlDuringRotation su Android per evitare crash in rotazione durante lo streaming Flutter. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,28 +31,32 @@ module Mediamtx
|
||||
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).
|
||||
body = recording_body(session, enabled: false).merge(
|
||||
source: "publisher",
|
||||
overridePublisher: true,
|
||||
alwaysAvailable: true,
|
||||
alwaysAvailableFile: slate_file_path
|
||||
overridePublisher: true
|
||||
)
|
||||
# YouTube: relay continuo gestito da Streams::YoutubeRelay (ffmpeg in sidekiq/rails).
|
||||
body[:alwaysAvailable] = true
|
||||
body[:alwaysAvailableFile] = slate_file_path
|
||||
# YouTube: telefono → MediaMTX; relay copy verso RTMPS in sidekiq.
|
||||
response = @conn.post("/v3/config/paths/add/#{CGI.escape(path)}", body)
|
||||
unless response.success?
|
||||
err = response.body.is_a?(Hash) ? response.body["error"] : response.body
|
||||
raise Error, "MediaMTX path create failed: #{response.status} #{err}"
|
||||
end
|
||||
remember_always_available(path, enabled: true)
|
||||
true
|
||||
end
|
||||
|
||||
def delete_path(session)
|
||||
delete_path_name(session.mediamtx_path_name)
|
||||
delete_path_name(session.mediamtx_overlay_path_name)
|
||||
forget_always_available(session.mediamtx_path_name)
|
||||
end
|
||||
|
||||
def delete_path_name(path_name)
|
||||
@conn.post("/v3/config/paths/delete/#{CGI.escape(path_name)}")
|
||||
forget_always_available(path_name)
|
||||
end
|
||||
|
||||
def set_path_recording(session, enabled:)
|
||||
@@ -67,16 +71,26 @@ module Mediamtx
|
||||
end
|
||||
|
||||
def patch_path(session)
|
||||
set_always_available(session, enabled: true)
|
||||
end
|
||||
|
||||
# Slate alwaysAvailable: copertina sullo stesso path quando il telefono è offline.
|
||||
# Disattivare quando il publisher è in onda; riattivare in pausa/disconnessione.
|
||||
def set_always_available(session, enabled:)
|
||||
path = session.mediamtx_path_name
|
||||
body = {
|
||||
alwaysAvailable: true,
|
||||
alwaysAvailableFile: slate_file_path
|
||||
}
|
||||
return true if always_available_remembered?(path, enabled: enabled)
|
||||
|
||||
body = if enabled
|
||||
{ alwaysAvailable: true, alwaysAvailableFile: slate_file_path }
|
||||
else
|
||||
{ alwaysAvailable: false }
|
||||
end
|
||||
response = @conn.patch("/v3/config/paths/patch/#{CGI.escape(path)}", body)
|
||||
unless response.success?
|
||||
err = response.body.is_a?(Hash) ? response.body["error"] : response.body
|
||||
raise Error, "MediaMTX path patch failed: #{response.status} #{err}"
|
||||
raise Error, "MediaMTX alwaysAvailable patch failed: #{response.status} #{err}"
|
||||
end
|
||||
remember_always_available(path, enabled: enabled)
|
||||
true
|
||||
end
|
||||
|
||||
@@ -152,5 +166,35 @@ module Mediamtx
|
||||
def slate_file_path
|
||||
ENV.fetch("MEDIAMTX_SLATE_FILE", "/slates/offline.mp4")
|
||||
end
|
||||
|
||||
def always_available_remembered?(path, enabled:)
|
||||
redis.get(always_available_key(path)) == always_available_value(enabled)
|
||||
rescue Redis::BaseError
|
||||
false
|
||||
end
|
||||
|
||||
def remember_always_available(path, enabled:)
|
||||
redis.set(always_available_key(path), always_available_value(enabled), ex: 48.hours.to_i)
|
||||
rescue Redis::BaseError
|
||||
nil
|
||||
end
|
||||
|
||||
def forget_always_available(path)
|
||||
redis.del(always_available_key(path))
|
||||
rescue Redis::BaseError
|
||||
nil
|
||||
end
|
||||
|
||||
def always_available_key(path)
|
||||
"mediamtx:always_available:#{path}"
|
||||
end
|
||||
|
||||
def always_available_value(enabled)
|
||||
enabled ? "1" : "0"
|
||||
end
|
||||
|
||||
def redis
|
||||
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
37
backend/app/services/mediamtx/publisher_online.rb
Normal file
37
backend/app/services/mediamtx/publisher_online.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
module Mediamtx
|
||||
# Telefono connesso al path RTMP (non solo slate alwaysAvailable).
|
||||
module PublisherOnline
|
||||
module_function
|
||||
|
||||
def active?(session)
|
||||
active_path?(path_info(session))
|
||||
end
|
||||
|
||||
def path_info(session)
|
||||
Client.new.list_paths.find { |i| i["name"] == session.mediamtx_path_name }
|
||||
end
|
||||
|
||||
def active_path?(info)
|
||||
return false unless info
|
||||
|
||||
info["online"] == true && info.dig("source", "type") == "rtmpConn"
|
||||
end
|
||||
|
||||
def h264_video?(info)
|
||||
return false unless info
|
||||
|
||||
(info["tracks2"] || []).any? do |track|
|
||||
track["codec"] == "H264" && track.dig("codecProps", "width").to_i.positive?
|
||||
end
|
||||
end
|
||||
|
||||
def video_publishing?(session)
|
||||
info = path_info(session)
|
||||
return false unless active_path?(info)
|
||||
# Slate alwaysAvailable ha H264 ma non è il telefono.
|
||||
return false unless info.dig("source", "type") == "rtmpConn"
|
||||
|
||||
h264_video?(info)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,40 +10,47 @@ module Mediamtx
|
||||
return @session if @session.terminal?
|
||||
|
||||
info = Mediamtx::Client.new.list_paths.find { |i| i["name"] == @session.mediamtx_path_name }
|
||||
publisher_online = publisher_online?(info)
|
||||
publisher_online = Mediamtx::PublisherOnline.active_path?(info)
|
||||
|
||||
if publisher_online
|
||||
clear_publisher_misses!(@session.id)
|
||||
if @session.paused?
|
||||
# RTMP ancora connesso in pausa: non forzare live/reconnect.
|
||||
elsif @session.may_go_live?
|
||||
@session.go_live!
|
||||
@session.reload
|
||||
Streams::Overlay::Refresh.call(@session)
|
||||
Streams::YoutubeRelay.ensure_publishing!(@session) if @session.platform == "youtube"
|
||||
elsif @session.reconnecting? && @session.may_reconnect?
|
||||
@session.reconnect!
|
||||
cancel_timeout
|
||||
Streams::Overlay::Refresh.call(@session.reload)
|
||||
else
|
||||
enable_live_path!(@session)
|
||||
if @session.may_go_live?
|
||||
@session.go_live!
|
||||
@session.reload
|
||||
kick_youtube!(@session) if @session.platform == "youtube"
|
||||
elsif @session.reconnecting? && @session.may_reconnect?
|
||||
@session.reconnect!
|
||||
cancel_timeout
|
||||
kick_youtube!(@session)
|
||||
end
|
||||
kick_youtube!(@session) if @session.platform == "youtube"
|
||||
end
|
||||
elsif !@session.paused? && @session.live? && @session.may_lose_connection?
|
||||
@session.lose_connection!
|
||||
schedule_timeout unless @session.paused?
|
||||
if publisher_offline_sustained?(@session.id)
|
||||
clear_publisher_misses!(@session.id)
|
||||
restore_slate_path!(@session)
|
||||
@session.lose_connection!
|
||||
schedule_timeout unless @session.paused?
|
||||
end
|
||||
elsif !publisher_online && @session.status.in?(%w[reconnecting connecting]) && !@session.paused?
|
||||
restore_slate_path!(@session)
|
||||
end
|
||||
|
||||
@session.reload
|
||||
@session.reload.tap do |session|
|
||||
schedule_youtube_relay_if_needed(session)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def publisher_online?(info)
|
||||
return false unless info
|
||||
def kick_youtube!(session)
|
||||
return unless Youtube::LivePipeline.pipeline_eligible?(session)
|
||||
|
||||
return true if info["online"]
|
||||
return true if info["source"].present?
|
||||
# MediaMTX a volte lascia online=false con publisher attivo; i byte in ingresso confermano il segnale.
|
||||
return true if info["ready"] && info["bytesReceived"].to_i > 1_000_000
|
||||
|
||||
false
|
||||
Youtube::LivePipeline.tick_session!(session)
|
||||
end
|
||||
|
||||
def cancel_timeout
|
||||
@@ -60,5 +67,55 @@ 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
|
||||
|
||||
# Match Live TV: lascia alwaysAvailable attivo — MediaMTX usa il publisher quando c'è
|
||||
# e la copertina nei gap RTMP (telefono instabile). Disattivare la slate lasciava HLS
|
||||
# senza stream ("no stream is available") e il player web in pausa/spinner.
|
||||
def enable_live_path!(session)
|
||||
return unless session.platform == "youtube"
|
||||
|
||||
Client.new.set_always_available(session, enabled: false)
|
||||
rescue Client::Error => e
|
||||
Rails.logger.warn("[PublisherSync] disable slate session=#{session.id}: #{e.message}")
|
||||
end
|
||||
|
||||
def restore_slate_path!(session)
|
||||
return if session.platform == "matchlivetv"
|
||||
|
||||
Client.new.set_always_available(session, enabled: true)
|
||||
rescue Client::Error => e
|
||||
Rails.logger.warn("[PublisherSync] enable slate session=#{session.id}: #{e.message}")
|
||||
end
|
||||
|
||||
# Evita reconnect su un singolo poll offline (RTMP instabile dal telefono).
|
||||
def publisher_offline_sustained?(session_id)
|
||||
misses = redis.incr(format("publisher_offline_miss:%s", session_id)).to_i
|
||||
redis.expire(format("publisher_offline_miss:%s", session_id), 120)
|
||||
misses >= 4
|
||||
end
|
||||
|
||||
def clear_publisher_misses!(session_id)
|
||||
redis.del(format("publisher_offline_miss:%s", session_id))
|
||||
end
|
||||
|
||||
def schedule_youtube_relay_if_needed(session)
|
||||
return unless session.platform == "youtube"
|
||||
return unless session.status.in?(%w[connecting live reconnecting paused])
|
||||
return unless Youtube::LivePipeline.broadcast_ready?(session)
|
||||
|
||||
key = format("youtube_relay:sched:%s", session.id)
|
||||
return unless redis.set(key, "1", nx: true, ex: 10)
|
||||
|
||||
YoutubeRelayEnsureJob.perform_later(session.id)
|
||||
end
|
||||
|
||||
def redis
|
||||
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user