Files
MatchLiveTv/backend/app/models/stream_session.rb
eminuxandCursor cc96c0396a Migliora sessioni admin e rende robusto il logout web.
Aggiunge filtri/colonne (società, orari) e dettaglio leggibile; evita 422 CSRF su logout e non cancella le cover slate custom in sync prod.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 20:41:12 +02:00

223 lines
5.7 KiB
Ruby

class StreamSession < ApplicationRecord
include AASM
PLATFORMS = %w[matchlivetv youtube facebook twitch].freeze
STATUSES = %w[idle connecting live reconnecting paused ended error].freeze
PRIVACY_STATUSES = %w[public unlisted private].freeze
belongs_to :match
belongs_to :user
belongs_to :stream_node, optional: true
has_many :stream_events, dependent: :destroy
has_one :score_state, dependent: :destroy
has_one :recording
has_many :device_states, dependent: :destroy
validates :platform, inclusion: { in: PLATFORMS }
validates :status, inclusion: { in: STATUSES }
validates :privacy_status, inclusion: { in: PRIVACY_STATUSES }
validates :min_quality_preset, inclusion: { in: Sessions::SelectQuality::MIN_QUALITY_IDS }
before_validation :normalize_privacy_status
before_validation :ensure_publish_token, on: :create
scope :broadcasting, -> { where(status: %w[live connecting reconnecting paused]) }
scope :publicly_listed, -> { where(privacy_status: "public") }
scope :search_by_team_or_opponent, lambda { |query|
q = query.to_s.strip
return all if q.blank?
term = "%#{sanitize_sql_like(q)}%"
joins(match: { team: :club }).where(
"teams.name ILIKE :term OR matches.opponent_name ILIKE :term OR clubs.name ILIKE :term OR matches.location ILIKE :term",
term: term
)
}
aasm column: :status do
state :idle, initial: true
state :connecting, :live, :reconnecting, :paused, :ended, :error
event :begin_connect do
transitions from: %i[idle paused], to: :connecting
end
event :go_live do
transitions from: %i[connecting reconnecting], to: :live
after { update!(started_at: Time.current) if started_at.nil? }
end
event :lose_connection do
transitions from: :live, to: :reconnecting
after { increment!(:disconnection_count) }
end
event :reconnect do
transitions from: :reconnecting, to: :live
end
event :pause do
transitions from: %i[live reconnecting connecting], to: :paused
end
event :resume do
transitions from: :paused, to: :connecting
end
event :finish do
transitions from: %i[live reconnecting paused connecting], to: :ended
after { close_session! }
end
event :fail do
transitions from: %i[connecting reconnecting idle], to: :error
end
end
def rtmp_ingest_url
# RootEncoder richiede rtmp://host:port/app/stream (due segmenti).
# MediaMTX path = live/match_{uuid} (no ?token= nel path).
"#{rtmp_base_url.chomp('/')}/#{mediamtx_path_name}"
end
def mediamtx_path_name
"live/match_#{id}"
end
def mediamtx_overlay_path_name
"#{mediamtx_path_name}_air"
end
def matchlivetv_platform?
platform == "matchlivetv"
end
def hls_playback_url
"#{hls_base_url.chomp('/')}/#{effective_hls_path_name}/index.m3u8"
end
def rtmp_base_url
stream_node&.rtmp_base_url.presence || MatchLiveTv.mediamtx_rtmp_url
end
def hls_base_url
stream_node&.hls_base_url.presence || MatchLiveTv.hls_public_url
end
def mediamtx_api_base_url
stream_node&.api_base_url.presence || MatchLiveTv.mediamtx_api_url
end
def mediamtx_internal_rtmp_url
stream_node&.internal_rtmp_url.presence ||
ENV.fetch("MEDIAMTX_INTERNAL_RTMP_URL", "rtmp://mediamtx:1935")
end
def mediamtx_internal_hls_url
stream_node&.internal_hls_url.presence ||
ENV.fetch("MEDIAMTX_HLS_URL", "http://mediamtx:8888")
end
def effective_hls_path_name
mediamtx_path_name
end
def watch_page_url
"#{MatchLiveTv.app_public_url.chomp('/')}/live/#{id}"
end
def youtube_watch_url
return nil if youtube_broadcast_id.blank?
return nil if youtube_broadcast_id.to_s.start_with?("mock_")
"https://www.youtube.com/watch?v=#{youtube_broadcast_id}"
end
def share_url
return youtube_watch_url if platform == "youtube"
watch_page_url
end
def youtube_ready?
platform == "youtube" && youtube_watch_url.present?
end
def link_only?
privacy_status.in?(%w[unlisted private])
end
def publicly_listed?
privacy_status == "public"
end
def terminal?
status.in?(%w[ended error])
end
def regia_token_active?
regia_token_digest.present? &&
regia_token_expires_at&.future? &&
!terminal?
end
def resumable?
status.in?(%w[connecting live reconnecting paused])
end
def end_stream!
return if terminal?
if may_finish?
finish!
else
update!(status: "ended")
record_ended_timestamps!
end
end
def stream_key
return @stream_key if instance_variable_defined?(:@stream_key)
return nil if stream_key_encrypted.blank?
stream_key_encryptor.decrypt_and_verify(stream_key_encrypted)
rescue ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
def stream_key=(value)
@stream_key = value
self.stream_key_encrypted =
value.present? ? stream_key_encryptor.encrypt_and_sign(value) : nil
end
private
def stream_key_encryptor
key = ActiveSupport::KeyGenerator.new(Rails.application.secret_key_base)
.generate_key("stream_session_stream_key", 32)
ActiveSupport::MessageEncryptor.new(key)
end
def ensure_publish_token
self.publish_token ||= SecureRandom.urlsafe_base64(32)
end
def normalize_privacy_status
self.privacy_status = "public" if privacy_status.blank?
self.privacy_status = "unlisted" if privacy_status == "private"
end
def record_ended_timestamps!
now = Time.current
update!(ended_at: now) if ended_at.nil?
if started_at && total_duration_secs.to_i.zero?
update!(total_duration_secs: (now - started_at).to_i)
end
end
def close_session!
record_ended_timestamps!
end
end