Files
MatchLiveTv/backend/app/controllers/api/v1/stream_sessions_controller.rb
Emiliano Frascaro 2b7e8b9e36 Migliora overlay basket Android, regia e catalogo sport senza timer.
Fix crash e race su +3, layout tabellone con squadre ai lati, safe area in diretta e rimozione overlay cronometro; backend con sync punteggio periodo, link diretta in regia e init score_state basket.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 23:31:12 +02:00

225 lines
7.6 KiB
Ruby

module Api
module V1
class StreamSessionsController < BaseController
before_action :set_session, except: :create
def create
team_ids = current_user.streamable_teams.map(&:id)
match = Match.where(team_id: team_ids).find(params[:match_id])
session = Sessions::Create.new(user: current_user, match: match, params: session_params).call
render json: session_json(session), status: :created
end
def show
render json: session_json(@session, detail: true)
end
def start
Sessions::Start.new(@session).call
render json: session_json(@session)
end
def stop
Sessions::Stop.new(@session).call
render json: session_json(@session)
end
def pause
Sessions::Pause.new(@session).call
render json: session_json(@session)
end
def resume
Sessions::Resume.new(@session).call
render json: session_json(@session)
end
def score
Scoring::SyncState.new(session: @session, payload: score_sync_params).call
render json: session_json(@session, detail: true)
end
def score_action
action = params[:score_action].presence
raise ArgumentError, "Azione mancante" if action.blank?
result = Scoring::ApplyAction.new(session: @session, action: action).call
render json: session_json(@session, detail: true).merge(
set_won: result.set_won,
match_won: result.match_won,
winner: result.winner&.to_s
)
rescue ArgumentError => e
render json: { error: e.message }, status: :unprocessable_entity
end
def events
events = @session.stream_events.recent.limit(100)
render json: events.map { |e| event_json(e) }
end
def telemetry
role = params.require(:device_role)
state = @session.device_states.find_or_initialize_by(device_role: role)
state.update!(
battery_level: params[:battery_level],
network_type: params[:network_type],
signal_strength: params[:signal_strength],
current_bitrate: params[:current_bitrate],
target_bitrate: params[:target_bitrate],
fps: params[:fps],
last_seen_at: Time.current
)
sync_publisher_when_streaming!(params[:fps].to_f)
SessionChannel.broadcast_message(@session, state.as_cable_payload)
head :no_content
end
def pairing_token
token = SecureRandom.urlsafe_base64(24)
@session.update!(
pairing_token_digest: Digest::SHA256.hexdigest(token),
pairing_token_expires_at: 15.minutes.from_now
)
render json: {
pairing_token: token,
expires_at: @session.pairing_token_expires_at,
qr_payload: {
session_id: @session.id,
pairing_token: token,
api_url: request.base_url
}
}
end
def regia_link
access = Sessions::RegiaAccess.new(@session)
token = access.issue_token!
render json: {
regia_url: access.url(token),
expires_at: @session.regia_token_expires_at
}
end
def claim_pairing
token = params.require(:pairing_token)
digest = Digest::SHA256.hexdigest(token)
unless @session.pairing_token_digest == digest &&
@session.pairing_token_expires_at&.future?
return render json: { error: "Invalid or expired token" }, status: :unauthorized
end
@session.device_states.find_or_create_by!(device_role: params[:device_role] || "controller")
render json: session_json(@session)
end
def network_test
upload_mbps = params[:upload_mbps].to_f
preset = Sessions::SelectQuality.call(upload_mbps: upload_mbps)
@session.update!(
quality_preset: preset[:id],
target_bitrate: preset[:target_bitrate],
target_fps: preset[:target_fps]
)
@session.stream_events.create!(
event_type: "network_test",
metadata: params.permit(:download_mbps, :upload_mbps, :latency_ms, :network_type)
.to_h.merge(
"quality_preset" => preset[:id],
"target_bitrate" => preset[:target_bitrate]
),
occurred_at: Time.current
)
target_upload = preset[:target_bitrate] / 1_000_000.0
ready = upload_mbps >= (target_upload * Sessions::SelectQuality::UPLOAD_HEADROOM)
render json: {
ready: ready,
target_upload_mbps: target_upload,
quality_preset: preset[:id],
target_bitrate: preset[:target_bitrate],
target_fps: preset[:target_fps]
}
end
def youtube_stats
count = if @session.platform == "youtube" && @session.youtube_broadcast_id.present?
Youtube::BroadcastService.new(@session.match.team)
.viewer_count(@session.youtube_broadcast_id)
else
0
end
render json: { concurrent_viewers: count, live: @session.live? }
end
private
def set_session
team_ids = current_user.streamable_teams.map(&:id)
@session = StreamSession.joins(:match)
.where(matches: { team_id: team_ids })
.find(params[:id])
end
def session_params
params.permit(:platform, :privacy_status, :quality_preset, :target_bitrate, :target_fps, :youtube_channel)
end
def score_sync_params
params.permit(
:home_sets, :away_sets, :home_points, :away_points, :current_set,
:clock_secs, :clock_running, :period, :home_score, :away_score, :overtime,
set_partials: %i[set home away]
)
end
def session_json(session, detail: false)
data = {
id: session.id,
match_id: session.match_id,
status: session.status,
platform: session.platform,
rtmp_ingest_url: session.rtmp_ingest_url,
hls_playback_url: session.hls_playback_url,
watch_page_url: session.matchlivetv_platform? ? session.watch_page_url : nil,
share_url: session.share_url,
youtube_watch_url: session.youtube_watch_url,
youtube_broadcast_id: session.youtube_broadcast_id,
youtube_ready: session.youtube_ready?,
privacy_status: session.privacy_status,
quality_preset: session.quality_preset,
target_bitrate: session.target_bitrate,
started_at: session.started_at,
disconnection_count: session.disconnection_count
}
if detail
data[:score] = session.score_state&.as_cable_payload
data[:devices] = session.device_states.map(&:as_cable_payload)
end
data
end
def event_json(event)
{
id: event.id,
event_type: event.event_type,
metadata: event.metadata,
occurred_at: event.occurred_at
}
end
# L'app invia fps ogni ~10s; poll status.json per allineare stato sessione e relay YouTube.
def sync_publisher_when_streaming!(fps)
return if fps < 1
return if @session.terminal? || @session.paused?
return unless @session.platform == "youtube"
return unless @session.status.in?(%w[connecting live reconnecting])
Mediamtx::PublisherSync.new(@session).call
if @session.platform == "youtube" && @session.status.in?(%w[live connecting reconnecting])
Mediamtx::PublisherSync.schedule_youtube_pipeline!(@session, force: false)
end
end
end
end
end