class StreamSession < ApplicationRecord include AASM PLATFORMS = %w[matchlivetv youtube facebook twitch].freeze STATUSES = %w[idle connecting live reconnecting paused ended error].freeze belongs_to :match belongs_to :user has_many :stream_events, dependent: :destroy has_one :score_state, dependent: :destroy has_many :device_states, dependent: :destroy validates :platform, inclusion: { in: PLATFORMS } validates :status, inclusion: { in: STATUSES } before_validation :ensure_publish_token, on: :create scope :broadcasting, -> { where(status: %w[live connecting reconnecting]) } 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).where( "teams.name ILIKE :term OR matches.opponent_name 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: :live, to: :paused end event :resume do transitions from: :paused, to: :live 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). "#{MatchLiveTv.mediamtx_rtmp_url}/#{mediamtx_path_name}" end def mediamtx_path_name "live/match_#{id}" end def matchlivetv_platform? platform == "matchlivetv" end def hls_playback_url base = MatchLiveTv.hls_public_url.chomp("/") "#{base}/#{mediamtx_path_name}/index.m3u8" end def watch_page_url "#{MatchLiveTv.app_public_url.chomp('/')}/live/#{id}" end def terminal? status.in?(%w[ended error]) 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 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