class HlsProxyController < ActionController::Base # Proxy pubblico verso MediaMTX HLS. # Dev: tutto passa da Rails (/hls/*). # Prod: edge inoltra /hls/ a MediaMTX; il redirect cookie di MediaMTX punta a # /live/match_{uuid}/… che deve essere gestito da #live_match (fallback). def show proxy_mediamtx_path(params[:path].to_s) end def live_match session_id = params[:session_id].to_s return head :not_found unless session_id.match?(/\A[0-9a-f-]{36}\z/i) segments = params[:segments].presence || "index.m3u8" proxy_mediamtx_path("live/match_#{session_id}/#{segments}") end private def proxy_mediamtx_path(upstream_path) return head :not_found if upstream_path.blank? upstream = "#{MatchLiveTv.mediamtx_hls_url}/#{upstream_path}" upstream = "#{upstream}?#{request.query_string}" if request.query_string.present? cookie = request.headers["Cookie"].presence || "cookieCheck=1" 5.times do response = hls_conn.get(upstream) { |req| req.headers["Cookie"] = cookie } if response.status.in?([301, 302, 307, 308]) location = response.headers["location"].to_s cookie = cookie_from_set_header(response.headers["set-cookie"]).presence || cookie upstream = resolve_upstream_url(location) next end return render_proxied_response(response) end head :bad_gateway rescue Faraday::Error => e Rails.logger.warn("[HlsProxy] #{upstream_path}: #{e.message}") head :bad_gateway end def resolve_upstream_url(location) return location if location.start_with?("http://", "https://") base = MatchLiveTv.mediamtx_hls_url.sub(%r{/$}, "") location.start_with?("/") ? "#{base}#{location}" : "#{base}/#{location}" end def render_proxied_response(response) body = response.body.to_s content_type = response.headers["content-type"].to_s if m3u8_playlist?(content_type, body) public_hls_base = "#{request.base_url}/hls" body = body.gsub(%r{(?<=["'(\s])/live/(match_[0-9a-f-]{36}/)}, "/hls/live/\\1") body = body.gsub(%r{\A/live/(match_[0-9a-f-]{36}/)}, "/hls/live/\\1") end response.headers.each do |key, value| next if %w[transfer-encoding connection content-length content-encoding].include?(key.downcase) if key.downcase == "location" headers[key] = rewrite_public_location(value) else headers[key] = value end end render body: body, status: response.status, content_type: content_type.presence end def rewrite_public_location(location) loc = location.to_s if loc.start_with?("/live/match_") return loc.sub(%r{\A/live/}, "/hls/") end if loc.include?("/live/match_") return loc.gsub(%r{/(live/match_[0-9a-f-]{36}/)}, "/hls/live/\\1") end loc end def m3u8_playlist?(content_type, body) return true if content_type.include?("mpegurl") || content_type.include?("m3u8") body.lstrip.start_with?("#EXTM3U") end def cookie_from_set_header(set_cookie_header) set_cookie_header.to_s[/cookieCheck=[^;]+/] end def hls_conn @hls_conn ||= Faraday.new do |f| f.adapter Faraday.default_adapter f.options.timeout = 15 f.options.open_timeout = 5 end end end