Initial commit: monorepo Match Live TV.
Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
150
backend/app/models/stream_session.rb
Normal file
150
backend/app/models/stream_session.rb
Normal file
@@ -0,0 +1,150 @@
|
||||
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
|
||||
Reference in New Issue
Block a user