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, 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