Merge branch 'feature/adaptive-bitrate' into collaudo

Integra bitrate adattivo e telemetria, con il mute audio come icona nella toolbar della regia.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-13 20:16:26 +02:00
co-authored by Cursor
65 changed files with 2727 additions and 138 deletions
+2
View File
@@ -59,6 +59,8 @@ class SessionChannel < ApplicationCable::Channel
Sessions::SetAudioMute.new(session, muted: true).call
when "unmute_audio"
Sessions::SetAudioMute.new(session, muted: false).call
when "set_min_quality"
Sessions::SetMinQuality.new(session, preset: data["min_quality_preset"]).call
end
end
@@ -74,6 +74,7 @@ module Api
current_bitrate: params[:current_bitrate],
target_bitrate: params[:target_bitrate],
fps: params[:fps],
thermal_state: sanitized_thermal_state,
last_seen_at: Time.current
)
sync_publisher_when_streaming!(params[:fps].to_f)
@@ -147,6 +148,13 @@ module Api
}
end
def min_quality
Sessions::SetMinQuality.new(@session, preset: params[:min_quality_preset]).call
render json: session_json(@session)
rescue ArgumentError => e
render json: { error: e.message }, status: :unprocessable_entity
end
def youtube_stats
count = if @session.platform == "youtube" && @session.youtube_broadcast_id.present?
Youtube::BroadcastService.new(@session.match.team)
@@ -166,6 +174,11 @@ module Api
.find(params[:id])
end
def sanitized_thermal_state
value = params[:thermal_state].to_s.downcase.presence
value if value.in?(%w[nominal fair serious critical])
end
def session_params
params.permit(:platform, :privacy_status, :quality_preset, :target_bitrate, :target_fps, :youtube_channel)
end
@@ -194,6 +207,7 @@ module Api
youtube_ready: session.youtube_ready?,
privacy_status: session.privacy_status,
quality_preset: session.quality_preset,
min_quality_preset: session.min_quality_preset,
target_bitrate: session.target_bitrate,
started_at: session.started_at,
disconnection_count: session.disconnection_count,
@@ -7,7 +7,7 @@ module Public
layout "regia"
protect_from_forgery with: :null_session
skip_before_action :verify_authenticity_token, only: %i[score pause resume stop audio_mute]
skip_before_action :verify_authenticity_token, only: %i[score pause resume stop audio_mute min_quality]
before_action :set_session_from_token
@@ -67,8 +67,20 @@ module Public
render json: status_payload
end
def min_quality
Sessions::SetMinQuality.new(@session, preset: params[:min_quality_preset]).call
render json: status_payload
rescue ArgumentError => e
render json: status_payload.merge(error: e.message), status: :unprocessable_entity
end
private
def camera_telemetry_payload(publisher_online)
cam = @session.device_states.find_by(device_role: "camera")
(cam&.as_regia_payload || {}).merge(publisher_online: publisher_online)
end
def set_session_from_token
@session = Sessions::RegiaAccess.find_session_by_token(params[:token])
return if @session
@@ -93,7 +105,10 @@ module Public
home_name: @session.match.team.name,
away_name: @session.match.opponent_name,
points_target: Scoring::Rules.points_target_for_match(@session.match, current_set: score&.current_set || 1),
score: score&.as_cable_payload
score: score&.as_cable_payload,
min_quality_preset: @session.min_quality_preset,
min_quality_options: Sessions::SelectQuality.allowed_min_ids(@session.quality_preset),
telemetry: camera_telemetry_payload(publisher_online)
}
end
end
+20 -1
View File
@@ -57,8 +57,14 @@ module Public
copyPrompt: t("regia.js.copy_prompt"),
shareRegiaText: t("regia.js.share_regia_text"),
shareLiveText: t("regia.js.share_live_text"),
minQualityUpdated: t("regia.js.min_quality_updated"),
minQualityError: t("regia.js.min_quality_error"),
minQualityAuto: t("regia.min_quality.auto"),
previewWaiting: t("regia.preview_waiting"),
streamEnded: t("regia.preview_ended"),
previewHide: t("regia.preview_hide"),
previewShow: t("regia.preview_show"),
previewOffHint: t("regia.preview_off_hint"),
resumeLabel: t("regia.resume"),
pauseLabel: t("regia.pause"),
muteLabel: t("regia.mute"),
@@ -67,7 +73,20 @@ module Public
pausedBadge: t("regia.status.paused"),
liveBadge: t("regia.status.live"),
waitingBadge: t("regia.status.waiting"),
subtitlePrefix: t("regia.subtitle_prefix")
subtitlePrefix: t("regia.subtitle_prefix"),
telemetryWaiting: t("regia.telemetry.waiting"),
telemetryStale: t("regia.telemetry.stale"),
telemetryJustNow: t("regia.telemetry.just_now"),
telemetrySecondsAgo: t("regia.telemetry.seconds_ago"),
telemetryPublisherOn: t("regia.telemetry.publisher_on"),
telemetryPublisherOff: t("regia.telemetry.publisher_off"),
telemetryThermalNominal: t("regia.telemetry.thermal_nominal"),
telemetryThermalFair: t("regia.telemetry.thermal_fair"),
telemetryThermalSerious: t("regia.telemetry.thermal_serious"),
telemetryThermalCritical: t("regia.telemetry.thermal_critical"),
telemetryBitrateMax: t("regia.telemetry.bitrate_max"),
telemetryBitrateTitle: t("regia.telemetry.bitrate_title"),
telemetryBitrateTap: t("regia.telemetry.bitrate_tap")
}.to_json
end
+13
View File
@@ -16,4 +16,17 @@ class DeviceState < ApplicationRecord
status: stream_session.status
}
end
def as_regia_payload
{
battery: battery_level,
network: network_type,
signal: signal_strength,
bitrate: current_bitrate,
target_bitrate: target_bitrate,
fps: fps,
thermal_state: thermal_state,
last_seen_at: last_seen_at&.iso8601
}
end
end
+1
View File
@@ -15,6 +15,7 @@ class StreamSession < ApplicationRecord
validates :platform, inclusion: { in: PLATFORMS }
validates :status, inclusion: { in: STATUSES }
validates :privacy_status, inclusion: { in: PRIVACY_STATUSES }
validates :min_quality_preset, inclusion: { in: Sessions::SelectQuality::MIN_QUALITY_IDS }
before_validation :normalize_privacy_status
before_validation :ensure_publish_token, on: :create
+4 -3
View File
@@ -9,11 +9,12 @@ module Sessions
@session.pause! if @session.may_pause?
Mediamtx::PublisherSync.forget_recording_state!(@session.id)
begin
Mediamtx::Client.for_session(@session).set_path_recording(@session, enabled: false)
mtx = Mediamtx::Client.for_session(@session)
mtx.set_path_recording(@session, enabled: false)
mtx.set_always_available(@session, enabled: true)
rescue Mediamtx::Client::Error => e
Rails.logger.warn("[Sessions::Pause] disable recording: #{e.message}")
Rails.logger.warn("[Sessions::Pause] MediaMTX: #{e.message}")
end
Mediamtx::Client.for_session(@session).set_always_available(@session, enabled: true)
# Slate su path per HLS in pausa; RTMP telefono si ferma via comando app.
log_event("paused")
SessionChannel.broadcast_message(@session, { type: "command", action: "pause_stream" })
@@ -3,6 +3,8 @@ module Sessions
class SelectQuality
UPLOAD_HEADROOM = 0.8
UPLOAD_HEADROOM_1080P = 1.0
AUTO_ID = "auto"
AUTO_FLOOR_BITRATE = 800_000
PRESETS = [
{ id: "1080p_30_4.5mbps", target_bitrate: 4_500_000, target_fps: 30 },
@@ -11,6 +13,32 @@ module Sessions
{ id: "720p_30_1.5mbps", target_bitrate: 1_500_000, target_fps: 30 }
].freeze
MIN_QUALITY_IDS = ([AUTO_ID] + PRESETS.map { |preset| preset[:id] }).freeze
def self.floor_bitrate(preset_id)
return AUTO_FLOOR_BITRATE if preset_id.blank? || preset_id == AUTO_ID
PRESETS.find { |preset| preset[:id] == preset_id }&.fetch(:target_bitrate) || AUTO_FLOOR_BITRATE
end
def self.allowed_min_ids(session_preset)
ids = [AUTO_ID] + PRESETS.reject { |preset| preset[:id].start_with?("1080p") }.reverse.map { |preset| preset[:id] }
ids << "1080p_30_4.5mbps" if session_preset.to_s.start_with?("1080p")
ids
end
def self.display_name(preset_id)
return AUTO_ID if preset_id.blank? || preset_id == AUTO_ID
preset = PRESETS.find { |item| item[:id] == preset_id }
return preset_id unless preset
resolution = preset[:id].start_with?("1080p") ? "1080p" : "720p"
mbps = preset[:target_bitrate] / 1_000_000.0
mbps_text = (mbps == mbps.to_i) ? mbps.to_i.to_s : format("%.1f", mbps)
"#{resolution} · #{mbps_text} Mbps"
end
def self.call(upload_mbps:)
new(upload_mbps: upload_mbps).call
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
module Sessions
class SetMinQuality
def initialize(session, preset:)
@session = session
@preset = preset.to_s.strip
end
def call
id = normalize
unless SelectQuality.allowed_min_ids(@session.quality_preset).include?(id)
raise ArgumentError, "Qualità minima non valida"
end
return @session if @session.min_quality_preset == id
@session.update!(min_quality_preset: id)
@session.stream_events.create!(
event_type: "quality_changed",
occurred_at: Time.current,
metadata: { "min_quality_preset" => id }
)
SessionChannel.broadcast_message(
@session,
{ type: "command", action: "set_min_quality", min_quality_preset: id }
)
@session
end
private
def normalize
return SelectQuality::AUTO_ID if @preset.blank? || @preset == SelectQuality::AUTO_ID
@preset
end
end
end
+1 -1
View File
@@ -7,7 +7,7 @@
<meta name="theme-color" content="#0a0a0e">
<title><%= content_for?(:title) ? yield(:title) : t("regia.meta_title_fallback") %></title>
<%= render "shared/meta_tags" %>
<link rel="stylesheet" href="/regia.css?v=1">
<link rel="stylesheet" href="/regia.css?v=11">
<%= yield :head %>
</head>
<body class="regia-body">
@@ -21,4 +21,4 @@
<button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:6px"></button>
</div>
</div>
<button type="button" class="regia-btn regia-btn--yellow" data-action="close_set" style="margin-top:12px"><%= t("regia.board.close_set") %></button>
<button type="button" class="regia-btn regia-btn--yellow" data-action="close_set"><%= t("regia.board.close_set") %></button>
+101 -33
View File
@@ -17,6 +17,8 @@
data-resume-url="<%= j public_regia_resume_path(params[:token]) %>"
data-audio-mute-url="<%= j public_regia_audio_mute_path(params[:token]) %>"
data-stop-url="<%= j public_regia_stop_path(params[:token]) %>"
data-min-quality-url="<%= j public_regia_min_quality_path(params[:token]) %>"
data-min-quality="<%= j @session.min_quality_preset %>"
data-cable-url="<%= j "/cable?regia_token=#{params[:token]}" %>"
data-hls-url="<%= j @session.hls_playback_url %>"
data-home-name="<%= j @team.name %>"
@@ -31,56 +33,122 @@
data-initial-audio-muted="<%= @session.audio_muted %>">
<header class="regia-header">
<h1><%= @team.name %> vs <%= @match.opponent_name %></h1>
<p id="regia-subtitle"><%= t("regia.subtitle_prefix") %> <%= regia_header_subtitle(@match, @score) %></p>
<div class="regia-header__text">
<h1><%= @team.name %> vs <%= @match.opponent_name %></h1>
<p id="regia-subtitle"><%= t("regia.subtitle_prefix") %> <%= regia_header_subtitle(@match, @score) %></p>
</div>
<span id="regia-badge" class="regia-badge <%= @stream_closed ? 'regia-badge--ended' : (@session.paused? ? 'regia-badge--wait' : (@on_air ? 'regia-badge--live' : 'regia-badge--wait')) %>">
<%= @stream_closed ? t("regia.status.ended") : (@session.paused? ? t("regia.status.paused") : (@on_air ? t("regia.status.live") : t("regia.status.waiting"))) %>
</span>
</header>
<% if @live_share_url.present? %>
<div class="regia-share-group">
<p class="regia-share-label"><%= t("regia.live_share_label") %></p>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-live" style="margin:0">
<%= t("regia.share_live") %>
<nav class="regia-toolbar" aria-label="<%= t("regia.links_nav") %>">
<% if @live_share_url.present? %>
<div class="regia-tool-group" role="group" aria-label="<%= t("regia.live_share_label") %>">
<span class="regia-tool-group__label"><%= t("regia.live_share_label") %></span>
<div class="regia-tool-group__btns">
<button type="button" class="regia-icon-btn" id="btn-share-live" aria-label="<%= t("regia.share_live") %>" title="<%= t("regia.share_live") %>">
<svg class="icon-outline" viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
</button>
<button type="button" class="regia-icon-btn" id="btn-copy-live" aria-label="<%= t("regia.copy_live") %>" title="<%= t("regia.copy_live") %>">
<svg class="icon-outline" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
</div>
<% end %>
<div class="regia-tool-group" role="group" aria-label="<%= t("regia.regia_share_label") %>">
<span class="regia-tool-group__label"><%= t("regia.regia_share_label") %></span>
<div class="regia-tool-group__btns">
<button type="button" class="regia-icon-btn" id="btn-share-regia" aria-label="<%= t("regia.share_regia") %>" title="<%= t("regia.share_regia") %>">
<svg class="icon-outline" viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-live" style="margin:0">
<%= t("regia.copy_link") %>
<button type="button" class="regia-icon-btn" id="btn-copy-regia" aria-label="<%= t("regia.copy_regia") %>" title="<%= t("regia.copy_regia") %>">
<svg class="icon-outline" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
</div>
<% end %>
<div class="regia-share-group">
<p class="regia-share-label"><%= t("regia.regia_share_label") %></p>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-regia" style="margin:0">
<%= t("regia.share_regia") %>
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-regia" style="margin:0">
<%= t("regia.copy_link") %>
</button>
</div>
</div>
<div class="regia-preview">
<% unless @stream_closed %>
<video id="regia-preview" playsinline muted autoplay></video>
<div class="regia-tool-group regia-tool-group--stream" role="group" aria-label="<%= t("regia.stream_controls_label") %>">
<span class="regia-tool-group__label" aria-hidden="true">&nbsp;</span>
<div class="regia-tool-group__btns">
<button type="button" class="regia-icon-btn" id="btn-toggle-preview" aria-label="<%= t("regia.preview_hide") %>" title="<%= t("regia.preview_hide") %>" aria-pressed="true">
<svg class="icon-fill icon-eye" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
<svg class="icon-fill icon-eye-off" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78 3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
</button>
<button type="button" class="regia-icon-btn<%= " is-paused" if @session.paused? %>" id="btn-pause" aria-label="<%= @session.paused? ? t("regia.resume") : t("regia.pause") %>" title="<%= @session.paused? ? t("regia.resume") : t("regia.pause") %>">
<svg class="icon-fill icon-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
<svg class="icon-fill icon-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
</button>
<button type="button" class="regia-icon-btn<%= " is-muted" if @session.audio_muted %>" id="btn-audio-mute" aria-label="<%= @session.audio_muted ? t("regia.unmute") : t("regia.mute") %>" title="<%= @session.audio_muted ? t("regia.unmute") : t("regia.mute") %>">
<svg class="icon-fill icon-mic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5-3c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
<svg class="icon-fill icon-mic-off" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>
</button>
<button type="button" class="regia-icon-btn regia-icon-btn--danger" id="btn-stop" aria-label="<%= t("regia.stop") %>" title="<%= t("regia.stop") %>">
<svg class="icon-fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6h12v12H6z"/></svg>
</button>
</div>
</div>
<% end %>
<div id="regia-preview-placeholder" class="regia-preview__placeholder">
<%= @stream_closed ? t("regia.preview_ended") : t("regia.preview_waiting") %>
</nav>
<section class="regia-preview-wrap" id="regia-preview-wrap">
<div class="regia-preview" id="regia-preview-box">
<% unless @stream_closed %>
<video id="regia-preview" playsinline muted autoplay></video>
<% end %>
<div id="regia-preview-placeholder" class="regia-preview__placeholder">
<%= @stream_closed ? t("regia.preview_ended") : t("regia.preview_waiting") %>
</div>
</div>
</div>
</section>
<section class="regia-scoreboard">
<%= render regia_board_partial(@match) %>
</section>
<% unless @stream_closed %>
<button type="button" class="regia-btn regia-btn--outline" id="btn-pause"><%= @session.paused? ? t("regia.resume") : t("regia.pause") %></button>
<button type="button" class="regia-btn regia-btn--outline<%= @session.audio_muted ? ' is-muted' : '' %>" id="btn-audio-mute"><%= @session.audio_muted ? t("regia.unmute") : t("regia.mute") %></button>
<button type="button" class="regia-btn regia-btn--danger" id="btn-stop"><%= t("regia.stop") %></button>
<section class="regia-telemetry" id="regia-telemetry" aria-label="<%= t("regia.telemetry.label") %>">
<div class="regia-telemetry__row">
<span class="regia-chip" id="tel-battery" title="<%= t("regia.telemetry.battery") %>">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 4h-1V3h-6v1H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H8V6h8v14z"/></svg>
<span class="regia-chip__label"><%= t("regia.telemetry.battery") %></span>
<span class="regia-chip__value"></span>
</span>
<span class="regia-chip" id="tel-network" title="<%= t("regia.telemetry.network") %>">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/></svg>
<span class="regia-chip__label"><%= t("regia.telemetry.network") %></span>
<span class="regia-chip__value"></span>
</span>
<button type="button" class="regia-chip regia-chip--action" id="tel-bitrate" aria-expanded="false" aria-controls="regia-quality" title="<%= t("regia.telemetry.bitrate_tap") %>">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 13h2v-2H3v2zm4 4h2v-6H7v6zm4 4h2V7h-2v14zm4-8h2V9h-2v4zm4-4h2V5h-2v4z"/></svg>
<span class="regia-chip__label"><%= t("regia.telemetry.bitrate") %></span>
<span class="regia-chip__value"></span>
<span class="regia-chip__hint" hidden></span>
</button>
<span class="regia-chip" id="tel-fps" title="<%= t("regia.telemetry.fps") %>">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
<span class="regia-chip__label"><%= t("regia.telemetry.fps") %></span>
<span class="regia-chip__value"></span>
</span>
<span class="regia-chip" id="tel-thermal" title="<%= t("regia.telemetry.thermal") %>">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 13V5c0-1.66-1.34-3-3-3S9 3.34 9 5v8c-1.21.91-2 2.37-2 4 0 2.76 2.24 5 5 5s5-2.24 5-5c0-1.63-.79-3.09-2-4zm-3 8c-1.66 0-3-1.34-3-3 0-1.19.7-2.23 1.71-2.72L11 14.73V5c0-.55.45-1 1-1s1 .45 1 1v9.73l.29.18C14.3 15.77 15 16.81 15 18c0 1.66-1.34 3-3 3z"/></svg>
<span class="regia-chip__label"><%= t("regia.telemetry.thermal") %></span>
<span class="regia-chip__value"></span>
</span>
</div>
<div class="regia-quality" id="regia-quality" hidden>
<p class="regia-quality__hint"><%= t("regia.min_quality.recommend") %></p>
<label for="min-quality"><%= t("regia.min_quality.label") %></label>
<select id="min-quality" title="<%= t("regia.min_quality.hint") %>">
<% Sessions::SelectQuality.allowed_min_ids(@session.quality_preset).each do |id| %>
<option value="<%= id %>" <%= "selected" if @session.min_quality_preset == id %>>
<%= id == Sessions::SelectQuality::AUTO_ID ? t("regia.min_quality.auto") : Sessions::SelectQuality.display_name(id) %>
</option>
<% end %>
</select>
</div>
<p class="regia-telemetry__meta" id="tel-meta"><%= t("regia.telemetry.waiting") %></p>
</section>
<% end %>
</div>
@@ -97,4 +165,4 @@
</div>
</div>
<script src="/regia.js?v=9"></script>
<script src="/regia.js?v=19"></script>