Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fecccaafd | ||
|
|
bc2257efd2 | ||
|
|
4af4fa68ac | ||
|
|
45bdda7c95 | ||
|
|
b926531447 | ||
|
|
da6fc1d523 |
@@ -55,6 +55,10 @@ class SessionChannel < ApplicationCable::Channel
|
|||||||
Sessions::Pause.new(session).call
|
Sessions::Pause.new(session).call
|
||||||
when "resume_stream"
|
when "resume_stream"
|
||||||
Sessions::Resume.new(session).call
|
Sessions::Resume.new(session).call
|
||||||
|
when "mute_audio"
|
||||||
|
Sessions::SetAudioMute.new(session, muted: true).call
|
||||||
|
when "unmute_audio"
|
||||||
|
Sessions::SetAudioMute.new(session, muted: false).call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ module Api
|
|||||||
render json: session_json(@session)
|
render json: session_json(@session)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def audio_mute
|
||||||
|
muted = params.key?(:muted) ? params[:muted] : true
|
||||||
|
Sessions::SetAudioMute.new(@session, muted: muted).call
|
||||||
|
render json: session_json(@session)
|
||||||
|
end
|
||||||
|
|
||||||
def score
|
def score
|
||||||
Scoring::SyncState.new(session: @session, payload: score_sync_params).call
|
Scoring::SyncState.new(session: @session, payload: score_sync_params).call
|
||||||
render json: session_json(@session, detail: true)
|
render json: session_json(@session, detail: true)
|
||||||
@@ -189,7 +195,8 @@ module Api
|
|||||||
quality_preset: session.quality_preset,
|
quality_preset: session.quality_preset,
|
||||||
target_bitrate: session.target_bitrate,
|
target_bitrate: session.target_bitrate,
|
||||||
started_at: session.started_at,
|
started_at: session.started_at,
|
||||||
disconnection_count: session.disconnection_count
|
disconnection_count: session.disconnection_count,
|
||||||
|
audio_muted: session.audio_muted
|
||||||
}
|
}
|
||||||
if detail
|
if detail
|
||||||
data[:score] = session.score_state&.as_cable_payload
|
data[:score] = session.score_state&.as_cable_payload
|
||||||
|
|||||||
@@ -11,17 +11,8 @@ module Public
|
|||||||
load_club_billing_context_for_pricing
|
load_club_billing_context_for_pricing
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def support
|
||||||
|
@app_store_review_chrome = true
|
||||||
def load_club_billing_context_for_pricing
|
|
||||||
return unless logged_in?
|
|
||||||
|
|
||||||
@club = current_user.primary_club
|
|
||||||
return unless @club
|
|
||||||
|
|
||||||
@subscription = @club.subscription
|
|
||||||
@team = @club.teams.order(:name).first
|
|
||||||
@entitlements = @team&.entitlements
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def privacy
|
def privacy
|
||||||
@@ -38,5 +29,18 @@ module Public
|
|||||||
|
|
||||||
def pallavolo
|
def pallavolo
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_club_billing_context_for_pricing
|
||||||
|
return unless logged_in?
|
||||||
|
|
||||||
|
@club = current_user.primary_club
|
||||||
|
return unless @club
|
||||||
|
|
||||||
|
@subscription = @club.subscription
|
||||||
|
@team = @club.teams.order(:name).first
|
||||||
|
@entitlements = @team&.entitlements
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module Public
|
|||||||
layout "regia"
|
layout "regia"
|
||||||
|
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
skip_before_action :verify_authenticity_token, only: %i[score pause resume stop]
|
skip_before_action :verify_authenticity_token, only: %i[score pause resume stop audio_mute]
|
||||||
|
|
||||||
before_action :set_session_from_token
|
before_action :set_session_from_token
|
||||||
|
|
||||||
@@ -61,6 +61,12 @@ module Public
|
|||||||
render json: status_payload
|
render json: status_payload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def audio_mute
|
||||||
|
muted = params.key?(:muted) ? params[:muted] : !@session.audio_muted
|
||||||
|
Sessions::SetAudioMute.new(@session, muted: muted).call
|
||||||
|
render json: status_payload
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_session_from_token
|
def set_session_from_token
|
||||||
@@ -78,6 +84,7 @@ module Public
|
|||||||
{
|
{
|
||||||
status: @session.status,
|
status: @session.status,
|
||||||
paused: @session.paused?,
|
paused: @session.paused?,
|
||||||
|
audio_muted: @session.audio_muted,
|
||||||
stream_closed: closed,
|
stream_closed: closed,
|
||||||
live: !closed && (@session.live? || @session.paused? || publisher_online),
|
live: !closed && (@session.live? || @session.paused? || publisher_online),
|
||||||
on_air: playable || (!closed && @session.status.in?(%w[connecting live reconnecting paused])),
|
on_air: playable || (!closed && @session.status.in?(%w[connecting live reconnecting paused])),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module Public
|
|||||||
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
|
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
|
||||||
{ loc: "#{base}/squadre", changefreq: "daily", priority: "0.85" },
|
{ loc: "#{base}/squadre", changefreq: "daily", priority: "0.85" },
|
||||||
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
|
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
|
||||||
|
{ loc: "#{base}/support", changefreq: "yearly", priority: "0.3" },
|
||||||
{ loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" },
|
{ loc: "#{base}/cookie", changefreq: "yearly", priority: "0.3" },
|
||||||
{ loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" }
|
{ loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -44,9 +44,12 @@ module Public
|
|||||||
partialsPrefix: t("regia.board.partials_prefix"),
|
partialsPrefix: t("regia.board.partials_prefix"),
|
||||||
resumed: t("regia.js.resumed"),
|
resumed: t("regia.js.resumed"),
|
||||||
pausedCover: t("regia.js.paused_cover"),
|
pausedCover: t("regia.js.paused_cover"),
|
||||||
|
muted: t("regia.js.muted"),
|
||||||
|
unmuted: t("regia.js.unmuted"),
|
||||||
closed: t("regia.js.closed"),
|
closed: t("regia.js.closed"),
|
||||||
resumeError: t("regia.js.resume_error"),
|
resumeError: t("regia.js.resume_error"),
|
||||||
pauseError: t("regia.js.pause_error"),
|
pauseError: t("regia.js.pause_error"),
|
||||||
|
muteError: t("regia.js.mute_error"),
|
||||||
closeError: t("regia.js.close_error"),
|
closeError: t("regia.js.close_error"),
|
||||||
closeConfirm: t("regia.js.close_confirm"),
|
closeConfirm: t("regia.js.close_confirm"),
|
||||||
linkUnavailable: t("regia.js.link_unavailable"),
|
linkUnavailable: t("regia.js.link_unavailable"),
|
||||||
@@ -58,6 +61,8 @@ module Public
|
|||||||
streamEnded: t("regia.preview_ended"),
|
streamEnded: t("regia.preview_ended"),
|
||||||
resumeLabel: t("regia.resume"),
|
resumeLabel: t("regia.resume"),
|
||||||
pauseLabel: t("regia.pause"),
|
pauseLabel: t("regia.pause"),
|
||||||
|
muteLabel: t("regia.mute"),
|
||||||
|
unmuteLabel: t("regia.unmute"),
|
||||||
endedBadge: t("regia.status.ended"),
|
endedBadge: t("regia.status.ended"),
|
||||||
pausedBadge: t("regia.status.paused"),
|
pausedBadge: t("regia.status.paused"),
|
||||||
liveBadge: t("regia.status.live"),
|
liveBadge: t("regia.status.live"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class StreamEvent < ApplicationRecord
|
class StreamEvent < ApplicationRecord
|
||||||
EVENT_TYPES = %w[
|
EVENT_TYPES = %w[
|
||||||
connected disconnected reconnected quality_changed error paused resumed
|
connected disconnected reconnected quality_changed error paused resumed
|
||||||
started ended network_test pairing youtube_ready
|
started ended network_test pairing youtube_ready audio_muted audio_unmuted
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
belongs_to :stream_session
|
belongs_to :stream_session
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
module Sessions
|
||||||
|
# Silenzia o riattiva il microfono della camera in onda.
|
||||||
|
# Lo stream continua a inviare AAC silenzioso (YouTube/MediaMTX richiedono la traccia audio).
|
||||||
|
class SetAudioMute
|
||||||
|
def initialize(session, muted:)
|
||||||
|
@session = session
|
||||||
|
@muted = ActiveModel::Type::Boolean.new.cast(muted)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
return @session if @session.audio_muted == @muted
|
||||||
|
|
||||||
|
@session.update!(audio_muted: @muted)
|
||||||
|
action = @muted ? "mute_audio" : "unmute_audio"
|
||||||
|
event_type = @muted ? "audio_muted" : "audio_unmuted"
|
||||||
|
log_event(event_type)
|
||||||
|
SessionChannel.broadcast_message(@session, { type: "command", action: action, muted: @muted })
|
||||||
|
SessionChannel.broadcast_message(@session, { type: "stream_event", event: "audio_muted", muted: @muted })
|
||||||
|
@session
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def log_event(type)
|
||||||
|
@session.stream_events.create!(event_type: type, occurred_at: Time.current, metadata: { muted: @muted })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,13 +12,13 @@
|
|||||||
</head>
|
</head>
|
||||||
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
||||||
<%= render "shared/cookie_banner" %>
|
<%= render "shared/cookie_banner" %>
|
||||||
<%= render "shared/marketing_nav" %>
|
<%= render(@app_store_review_chrome ? "shared/marketing_nav_app_store" : "shared/marketing_nav") %>
|
||||||
<main>
|
<main>
|
||||||
<% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %>
|
<% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %>
|
||||||
<% if flash[:alert] %><div class="wrap"><div class="flash alert"><%= flash[:alert] %></div></div><% end %>
|
<% if flash[:alert] %><div class="wrap"><div class="flash alert"><%= flash[:alert] %></div></div><% end %>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
</main>
|
</main>
|
||||||
<%= render "shared/marketing_footer" %>
|
<%= render(@app_store_review_chrome ? "shared/marketing_footer_app_store" : "shared/marketing_footer") %>
|
||||||
<script src="/branding-form.js?v=1" defer></script>
|
<script src="/branding-form.js?v=1" defer></script>
|
||||||
<script src="/roster-form.js?v=1" defer></script>
|
<script src="/roster-form.js?v=1" defer></script>
|
||||||
<script src="/password-toggle.js?v=2" defer></script>
|
<script src="/password-toggle.js?v=2" defer></script>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<% content_for :title, t("legal.support.title") %>
|
||||||
|
<% content_for :meta_description, t("legal.support.meta_description") %>
|
||||||
|
<% content_for :canonical_url, seo_absolute_url(public_support_path) %>
|
||||||
|
|
||||||
|
<div class="wrap legal-doc">
|
||||||
|
<h1><%= t("legal.support.h1") %></h1>
|
||||||
|
<p><%= t("legal.support.intro") %></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= t("legal.support.email_label") %>
|
||||||
|
<a href="mailto:<%= MatchLiveTv.support_email %>"><%= MatchLiveTv.support_email %></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2><%= t("legal.support.access_title") %></h2>
|
||||||
|
<p><%= t("legal.support.access_body") %></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2><%= t("legal.support.live_title") %></h2>
|
||||||
|
<p><%= t("legal.support.live_body") %></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2><%= t("legal.support.team_title") %></h2>
|
||||||
|
<p><%= t("legal.support.team_body") %></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2><%= t("legal.support.privacy_title") %></h2>
|
||||||
|
<p>
|
||||||
|
<%= raw t(
|
||||||
|
"legal.support.privacy_body_html",
|
||||||
|
privacy_link: link_to(t("legal.support.privacy_link_text"), public_privacy_path)
|
||||||
|
) %>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
data-score-url="<%= j public_regia_score_path(params[:token]) %>"
|
data-score-url="<%= j public_regia_score_path(params[:token]) %>"
|
||||||
data-pause-url="<%= j public_regia_pause_path(params[:token]) %>"
|
data-pause-url="<%= j public_regia_pause_path(params[:token]) %>"
|
||||||
data-resume-url="<%= j public_regia_resume_path(params[:token]) %>"
|
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-stop-url="<%= j public_regia_stop_path(params[:token]) %>"
|
||||||
data-cable-url="<%= j "/cable?regia_token=#{params[:token]}" %>"
|
data-cable-url="<%= j "/cable?regia_token=#{params[:token]}" %>"
|
||||||
data-hls-url="<%= j @session.hls_playback_url %>"
|
data-hls-url="<%= j @session.hls_playback_url %>"
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
data-live-share-title="<%= j @live_share_title %>"
|
data-live-share-title="<%= j @live_share_title %>"
|
||||||
data-points-target="<%= @match.effective_board_type.in?(%w[volley racket]) ? Scoring::Rules.from_match(@match).points_target(@score.current_set) : 0 %>"
|
data-points-target="<%= @match.effective_board_type.in?(%w[volley racket]) ? Scoring::Rules.from_match(@match).points_target(@score.current_set) : 0 %>"
|
||||||
data-stream-closed="<%= @stream_closed %>"
|
data-stream-closed="<%= @stream_closed %>"
|
||||||
data-initial-paused="<%= @session.paused? %>">
|
data-initial-paused="<%= @session.paused? %>"
|
||||||
|
data-initial-audio-muted="<%= @session.audio_muted %>">
|
||||||
|
|
||||||
<header class="regia-header">
|
<header class="regia-header">
|
||||||
<h1><%= @team.name %> vs <%= @match.opponent_name %></h1>
|
<h1><%= @team.name %> vs <%= @match.opponent_name %></h1>
|
||||||
@@ -77,6 +79,7 @@
|
|||||||
|
|
||||||
<% unless @stream_closed %>
|
<% 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" 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>
|
<button type="button" class="regia-btn regia-btn--danger" id="btn-stop"><%= t("regia.stop") %></button>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,4 +97,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/regia.js?v=8"></script>
|
<script src="/regia.js?v=9"></script>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<strong style="color:#fff">Match Live TV</strong> — <%= t("footer.tagline") %>
|
<strong style="color:#fff">Match Live TV</strong> — <%= t("footer.tagline") %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<%= link_to t("common.support"), public_support_path %> ·
|
||||||
<%= link_to t("common.pricing"), public_prezzi_path %> ·
|
<%= link_to t("common.pricing"), public_prezzi_path %> ·
|
||||||
<%= link_to t("footer.live"), public_live_index_path %> ·
|
<%= link_to t("footer.live"), public_live_index_path %> ·
|
||||||
<%= link_to t("common.faq"), public_faq_path %> ·
|
<%= link_to t("common.faq"), public_faq_path %> ·
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<%# Footer minimale per App Store Review: solo link legali/supporto, nessun CTA commerciale. %>
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="wrap">
|
||||||
|
<div>
|
||||||
|
<strong style="color:#fff">Match Live TV</strong> — <%= t("footer.tagline") %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<%= link_to t("common.support"), public_support_path %> ·
|
||||||
|
<%= link_to t("common.privacy"), public_privacy_path %> ·
|
||||||
|
<%= link_to t("common.cookies"), public_cookies_path %> ·
|
||||||
|
<%= link_to t("common.terms"), public_termini_path %>
|
||||||
|
· <button type="button" class="footer-link-btn" data-cookie-manage><%= t("footer.manage_cookies") %></button>
|
||||||
|
</div>
|
||||||
|
<div class="site-footer__legal">
|
||||||
|
<p><%= t("footer.copyright") %></p>
|
||||||
|
<p><%= t("footer.responsibility") %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<%# Chrome minimale per App Store Review: branding, lingua, Privacy e Supporto — senza CTA commerciali. %>
|
||||||
|
<div class="site-chrome">
|
||||||
|
<div class="site-masthead">
|
||||||
|
<div class="wrap mast-inner">
|
||||||
|
<%= link_to public_support_path, class: "mast-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
|
||||||
|
<img class="mast-brand-logo" src="/logo.png?v=3" alt="Match Live TV" width="40" height="40" decoding="async">
|
||||||
|
<span class="brand" aria-hidden="true">Match <span>Live TV</span></span>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="mast-tools">
|
||||||
|
<button type="button" class="nav-toggle" aria-label="<%= t('nav.open_menu') %>" aria-expanded="false" aria-controls="site-nav">
|
||||||
|
<span class="nav-toggle-bar" aria-hidden="true"></span>
|
||||||
|
<span class="nav-toggle-bar" aria-hidden="true"></span>
|
||||||
|
<span class="nav-toggle-bar" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
|
||||||
|
<div class="nav-panel">
|
||||||
|
<div class="nav-mobile-head">
|
||||||
|
<%= link_to public_support_path, class: "nav-mobile-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
|
||||||
|
<img class="nav-mobile-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
|
||||||
|
<span class="brand">Match <span>Live TV</span></span>
|
||||||
|
<% end %>
|
||||||
|
<div class="nav-lang">
|
||||||
|
<%= render "shared/language_switcher" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%= link_to t("common.support"), public_support_path, class: "nav-active" %>
|
||||||
|
<%= link_to t("common.privacy"), public_privacy_path, class: (request.path == "/privacy" ? "nav-active" : nil) %>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-backdrop" id="nav-backdrop" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var chrome = document.querySelector(".site-chrome");
|
||||||
|
var toggle = document.querySelector(".nav-toggle");
|
||||||
|
var backdrop = document.getElementById("nav-backdrop");
|
||||||
|
var nav = document.getElementById("site-nav");
|
||||||
|
if (!chrome || !toggle || !nav) return;
|
||||||
|
var openLabel = <%= raw t("nav.open_menu").to_json %>;
|
||||||
|
var closeLabel = <%= raw t("nav.close_menu").to_json %>;
|
||||||
|
|
||||||
|
function setOpen(open) {
|
||||||
|
chrome.classList.toggle("nav-open", open);
|
||||||
|
document.body.classList.toggle("nav-menu-open", open);
|
||||||
|
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
toggle.setAttribute("aria-label", open ? closeLabel : openLabel);
|
||||||
|
nav.setAttribute("aria-hidden", open ? "false" : "true");
|
||||||
|
if (backdrop) backdrop.setAttribute("aria-hidden", open ? "false" : "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu() { setOpen(false); }
|
||||||
|
|
||||||
|
toggle.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
document.body.classList.contains("nav-menu-open") ? closeMenu() : setOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (backdrop) backdrop.addEventListener("click", closeMenu);
|
||||||
|
|
||||||
|
function shouldCloseNavOnControl(el) {
|
||||||
|
if (!el.closest("[data-lang-switcher]")) return true;
|
||||||
|
return el.classList.contains("lang-switcher__option");
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.querySelectorAll("a, button").forEach(function (el) {
|
||||||
|
el.addEventListener("click", function () {
|
||||||
|
if (shouldCloseNavOnControl(el)) closeMenu();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", function () {
|
||||||
|
if (window.matchMedia("(min-width: 900px)").matches) closeMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape") closeMenu();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -81,6 +81,12 @@ module MatchLiveTv
|
|||||||
ENV.fetch("PRIVACY_CONTACT_EMAIL", "privacy@matchlivetv.it")
|
ENV.fetch("PRIVACY_CONTACT_EMAIL", "privacy@matchlivetv.it")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Email di supporto (App Store Support URL e contatti assistenza).
|
||||||
|
# Preferisce SUPPORT_CONTACT_EMAIL; default = contatto commerciale già usato nel sito.
|
||||||
|
def support_email
|
||||||
|
ENV["SUPPORT_CONTACT_EMAIL"].presence || "info@matchlivetv.it"
|
||||||
|
end
|
||||||
|
|
||||||
def privacy_controller_address
|
def privacy_controller_address
|
||||||
ENV.fetch("PRIVACY_CONTROLLER_ADDRESS", "Via Guido De Ruggiero, 89 - 20142 - Milano (MI)")
|
ENV.fetch("PRIVACY_CONTROLLER_ADDRESS", "Via Guido De Ruggiero, 89 - 20142 - Milano (MI)")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -441,6 +441,8 @@ de:
|
|||||||
preview_waiting: Vorschau wartet auf Signal…
|
preview_waiting: Vorschau wartet auf Signal…
|
||||||
pause: Pausieren
|
pause: Pausieren
|
||||||
resume: Livestream fortsetzen
|
resume: Livestream fortsetzen
|
||||||
|
mute: Audio stummschalten
|
||||||
|
unmute: Audio einschalten
|
||||||
stop: Livestream beenden
|
stop: Livestream beenden
|
||||||
modal:
|
modal:
|
||||||
set_won_title: Satz gewonnen
|
set_won_title: Satz gewonnen
|
||||||
@@ -476,9 +478,12 @@ de:
|
|||||||
unknown_period: "—"
|
unknown_period: "—"
|
||||||
resumed: Übertragung fortgesetzt
|
resumed: Übertragung fortgesetzt
|
||||||
paused_cover: "Übertragung pausiert — Deckbild auf Sendung"
|
paused_cover: "Übertragung pausiert — Deckbild auf Sendung"
|
||||||
|
muted: "Audio stumm — YouTube empfängt das Mikrofon nicht"
|
||||||
|
unmuted: "Mikrofon-Audio wieder aktiv"
|
||||||
closed: Übertragung beendet
|
closed: Übertragung beendet
|
||||||
resume_error: Fehler beim Fortsetzen der Übertragung
|
resume_error: Fehler beim Fortsetzen der Übertragung
|
||||||
pause_error: Fehler beim Pausieren der Übertragung
|
pause_error: Fehler beim Pausieren der Übertragung
|
||||||
|
mute_error: Fehler beim Stummschalten
|
||||||
close_error: Fehler beim Beenden
|
close_error: Fehler beim Beenden
|
||||||
close_confirm: Übertragung endgültig beenden?
|
close_confirm: Übertragung endgültig beenden?
|
||||||
link_unavailable: Link nicht verfügbar
|
link_unavailable: Link nicht verfügbar
|
||||||
|
|||||||
@@ -436,6 +436,8 @@ en:
|
|||||||
preview_waiting: Waiting for signal…
|
preview_waiting: Waiting for signal…
|
||||||
pause: Pause stream
|
pause: Pause stream
|
||||||
resume: Resume stream
|
resume: Resume stream
|
||||||
|
mute: Mute audio
|
||||||
|
unmute: Unmute audio
|
||||||
stop: End stream
|
stop: End stream
|
||||||
modal:
|
modal:
|
||||||
set_won_title: Set won
|
set_won_title: Set won
|
||||||
@@ -471,9 +473,12 @@ en:
|
|||||||
unknown_period: "—"
|
unknown_period: "—"
|
||||||
resumed: Stream resumed
|
resumed: Stream resumed
|
||||||
paused_cover: "Stream paused — cover on air"
|
paused_cover: "Stream paused — cover on air"
|
||||||
|
muted: "Audio muted — YouTube is not receiving the microphone"
|
||||||
|
unmuted: "Microphone audio restored"
|
||||||
closed: Stream closed
|
closed: Stream closed
|
||||||
resume_error: Error resuming the stream
|
resume_error: Error resuming the stream
|
||||||
pause_error: Error pausing the stream
|
pause_error: Error pausing the stream
|
||||||
|
mute_error: Error muting audio
|
||||||
close_error: Error closing
|
close_error: Error closing
|
||||||
close_confirm: Permanently close the live stream?
|
close_confirm: Permanently close the live stream?
|
||||||
link_unavailable: Link not available
|
link_unavailable: Link not available
|
||||||
|
|||||||
@@ -441,6 +441,8 @@ es:
|
|||||||
preview_waiting: Vista previa a la espera de señal…
|
preview_waiting: Vista previa a la espera de señal…
|
||||||
pause: Pausar directo
|
pause: Pausar directo
|
||||||
resume: Reanudar directo
|
resume: Reanudar directo
|
||||||
|
mute: Silenciar audio
|
||||||
|
unmute: Reactivar audio
|
||||||
stop: Finalizar directo
|
stop: Finalizar directo
|
||||||
modal:
|
modal:
|
||||||
set_won_title: Set ganado
|
set_won_title: Set ganado
|
||||||
@@ -476,9 +478,12 @@ es:
|
|||||||
unknown_period: "—"
|
unknown_period: "—"
|
||||||
resumed: Retransmisión reanudada
|
resumed: Retransmisión reanudada
|
||||||
paused_cover: "Retransmisión en pausa — cortinilla al aire"
|
paused_cover: "Retransmisión en pausa — cortinilla al aire"
|
||||||
|
muted: "Audio silenciado — YouTube no recibe el micrófono"
|
||||||
|
unmuted: "Audio del micrófono reactivado"
|
||||||
closed: Retransmisión finalizada
|
closed: Retransmisión finalizada
|
||||||
resume_error: Error al reanudar la retransmisión
|
resume_error: Error al reanudar la retransmisión
|
||||||
pause_error: Error al pausar la retransmisión
|
pause_error: Error al pausar la retransmisión
|
||||||
|
mute_error: Error al silenciar el audio
|
||||||
close_error: Error al cerrar
|
close_error: Error al cerrar
|
||||||
close_confirm: "¿Cerrar definitivamente la retransmisión?"
|
close_confirm: "¿Cerrar definitivamente la retransmisión?"
|
||||||
link_unavailable: Enlace no disponible
|
link_unavailable: Enlace no disponible
|
||||||
|
|||||||
@@ -441,6 +441,8 @@ fr:
|
|||||||
preview_waiting: Aperçu en attente du signal…
|
preview_waiting: Aperçu en attente du signal…
|
||||||
pause: Mettre en pause
|
pause: Mettre en pause
|
||||||
resume: Reprendre le direct
|
resume: Reprendre le direct
|
||||||
|
mute: Couper l'audio
|
||||||
|
unmute: Réactiver l'audio
|
||||||
stop: Terminer le direct
|
stop: Terminer le direct
|
||||||
modal:
|
modal:
|
||||||
set_won_title: Set gagné
|
set_won_title: Set gagné
|
||||||
@@ -476,9 +478,12 @@ fr:
|
|||||||
unknown_period: "—"
|
unknown_period: "—"
|
||||||
resumed: Direct repris
|
resumed: Direct repris
|
||||||
paused_cover: "Direct en pause — habillage à l'antenne"
|
paused_cover: "Direct en pause — habillage à l'antenne"
|
||||||
|
muted: "Audio coupé — YouTube ne reçoit pas le micro"
|
||||||
|
unmuted: "Audio du micro rétabli"
|
||||||
closed: Direct terminé
|
closed: Direct terminé
|
||||||
resume_error: Erreur lors de la reprise du direct
|
resume_error: Erreur lors de la reprise du direct
|
||||||
pause_error: Erreur lors de la mise en pause du direct
|
pause_error: Erreur lors de la mise en pause du direct
|
||||||
|
mute_error: Erreur lors de la coupure audio
|
||||||
close_error: Erreur de clôture
|
close_error: Erreur de clôture
|
||||||
close_confirm: Clôturer définitivement le direct ?
|
close_confirm: Clôturer définitivement le direct ?
|
||||||
link_unavailable: Lien non disponible
|
link_unavailable: Lien non disponible
|
||||||
|
|||||||
@@ -436,6 +436,8 @@ it:
|
|||||||
preview_waiting: Anteprima in attesa del segnale…
|
preview_waiting: Anteprima in attesa del segnale…
|
||||||
pause: Metti in pausa
|
pause: Metti in pausa
|
||||||
resume: Riprendi diretta
|
resume: Riprendi diretta
|
||||||
|
mute: Silenzia audio
|
||||||
|
unmute: Riattiva audio
|
||||||
stop: Chiudi diretta
|
stop: Chiudi diretta
|
||||||
modal:
|
modal:
|
||||||
set_won_title: Set vinto
|
set_won_title: Set vinto
|
||||||
@@ -471,9 +473,12 @@ it:
|
|||||||
unknown_period: "—"
|
unknown_period: "—"
|
||||||
resumed: Diretta ripresa
|
resumed: Diretta ripresa
|
||||||
paused_cover: "Diretta in pausa — copertina in onda"
|
paused_cover: "Diretta in pausa — copertina in onda"
|
||||||
|
muted: "Audio silenziato — YouTube non riceve il microfono"
|
||||||
|
unmuted: "Audio microfono riattivato"
|
||||||
closed: Diretta chiusa
|
closed: Diretta chiusa
|
||||||
resume_error: Errore ripresa diretta
|
resume_error: Errore ripresa diretta
|
||||||
pause_error: Errore pausa diretta
|
pause_error: Errore pausa diretta
|
||||||
|
mute_error: Errore silenziamento audio
|
||||||
close_error: Errore chiusura
|
close_error: Errore chiusura
|
||||||
close_confirm: Chiudere definitivamente la diretta?
|
close_confirm: Chiudere definitivamente la diretta?
|
||||||
link_unavailable: Link non disponibile
|
link_unavailable: Link non disponibile
|
||||||
|
|||||||
@@ -195,3 +195,18 @@ de:
|
|||||||
s6_body_html: "Um die von der DSGVO vorgesehenen Rechte auszuüben (Zugang, Löschung, Widerspruch, Widerruf der Einwilligung), schreiben Sie an %{email_link}. Details finden Sie im %{privacy_doc_link}."
|
s6_body_html: "Um die von der DSGVO vorgesehenen Rechte auszuüben (Zugang, Löschung, Widerspruch, Widerruf der Einwilligung), schreiben Sie an %{email_link}. Details finden Sie im %{privacy_doc_link}."
|
||||||
s6_privacy_doc_link_text: Datenschutzdokument
|
s6_privacy_doc_link_text: Datenschutzdokument
|
||||||
manage_button: Cookie-Einstellungen verwalten
|
manage_button: Cookie-Einstellungen verwalten
|
||||||
|
support:
|
||||||
|
title: Match Live TV Support
|
||||||
|
meta_description: "Match Live TV Hilfe: Zugangsprobleme, Livestreams, Teamverwaltung und Support-Kontaktdaten."
|
||||||
|
h1: Match Live TV Support
|
||||||
|
intro: "Brauchen Sie Hilfe mit Match Live TV? Bei Problemen mit dem Zugang, der Einrichtung von Livestreams, der Spielverwaltung oder der Nutzung der App können Sie unseren Support kontaktieren."
|
||||||
|
email_label: "Support-E-Mail:"
|
||||||
|
access_title: Zugangsprobleme
|
||||||
|
access_body: "Wenn Sie sich nicht anmelden können, prüfen Sie die Zugangsdaten, die Sie von Ihrem Sportverein erhalten haben. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support."
|
||||||
|
live_title: Probleme während eines Livestreams
|
||||||
|
live_body: "Prüfen Sie Ihre Internetverbindung und versuchen Sie erneut, die Übertragung in der App zu starten. Wenn das Problem weiterhin besteht, kontaktieren Sie den Support und geben Sie Gerät, App-Version und eine kurze Beschreibung des Problems an."
|
||||||
|
team_title: Teamverwaltung
|
||||||
|
team_body: "Konten und Übertragungsberechtigungen werden vom Sportverein verwaltet."
|
||||||
|
privacy_title: Datenschutz
|
||||||
|
privacy_body_html: "Informationen zur Verarbeitung personenbezogener Daten finden Sie auf der Seite %{privacy_link}."
|
||||||
|
privacy_link_text: Datenschutz
|
||||||
|
|||||||
@@ -195,3 +195,18 @@ en:
|
|||||||
s6_body_html: "To exercise the rights provided by the GDPR (access, erasure, objection, withdrawal of consent) write to %{email_link}. Details are in the %{privacy_doc_link}."
|
s6_body_html: "To exercise the rights provided by the GDPR (access, erasure, objection, withdrawal of consent) write to %{email_link}. Details are in the %{privacy_doc_link}."
|
||||||
s6_privacy_doc_link_text: privacy document
|
s6_privacy_doc_link_text: privacy document
|
||||||
manage_button: Manage cookie preferences
|
manage_button: Manage cookie preferences
|
||||||
|
support:
|
||||||
|
title: Match Live TV Support
|
||||||
|
meta_description: "Match Live TV help: access issues, live streaming, team management and support contact details."
|
||||||
|
h1: Match Live TV Support
|
||||||
|
intro: "Need help with Match Live TV? For access issues, live stream setup, match management or app usage, you can contact our support team."
|
||||||
|
email_label: "Support email:"
|
||||||
|
access_title: Access issues
|
||||||
|
access_body: "If you cannot sign in, check the credentials provided by your sports club. If the problem persists, contact support."
|
||||||
|
live_title: Issues during a live stream
|
||||||
|
live_body: "Check your Internet connection and try starting the broadcast from the app again. If the problem persists, contact support and include your device, app version and a short description of the issue."
|
||||||
|
team_title: Team management
|
||||||
|
team_body: "Accounts and broadcasting permissions are managed by the sports club."
|
||||||
|
privacy_title: Privacy
|
||||||
|
privacy_body_html: "For information on personal data processing, see the %{privacy_link} page."
|
||||||
|
privacy_link_text: Privacy
|
||||||
|
|||||||
@@ -195,3 +195,18 @@ es:
|
|||||||
s6_body_html: "Para ejercer los derechos previstos por el RGPD (acceso, supresión, oposición, revocación del consentimiento) escribe a %{email_link}. Más detalles en el %{privacy_doc_link}."
|
s6_body_html: "Para ejercer los derechos previstos por el RGPD (acceso, supresión, oposición, revocación del consentimiento) escribe a %{email_link}. Más detalles en el %{privacy_doc_link}."
|
||||||
s6_privacy_doc_link_text: documento de privacidad
|
s6_privacy_doc_link_text: documento de privacidad
|
||||||
manage_button: Gestionar preferencias de cookies
|
manage_button: Gestionar preferencias de cookies
|
||||||
|
support:
|
||||||
|
title: Soporte Match Live TV
|
||||||
|
meta_description: "Ayuda de Match Live TV: problemas de acceso, directos, gestión del equipo y datos de contacto del soporte."
|
||||||
|
h1: Soporte Match Live TV
|
||||||
|
intro: "¿Necesitas ayuda con Match Live TV? Para problemas de acceso, configuración de directos, gestión de partidos o uso de la app puedes contactar con nuestro soporte."
|
||||||
|
email_label: "Correo de soporte:"
|
||||||
|
access_title: Problemas de acceso
|
||||||
|
access_body: "Si no puedes acceder, verifica las credenciales recibidas de tu club deportivo. Si el problema continúa, contacta con el soporte."
|
||||||
|
live_title: Problemas durante un directo
|
||||||
|
live_body: "Verifica la conexión a Internet e intenta de nuevo iniciar la transmisión desde la app. Si el problema continúa, contacta con el soporte indicando el dispositivo, la versión de la app y una breve descripción del problema."
|
||||||
|
team_title: Gestión del equipo
|
||||||
|
team_body: "Las cuentas y los permisos de transmisión los gestiona el club deportivo."
|
||||||
|
privacy_title: Privacidad
|
||||||
|
privacy_body_html: "Para información sobre el tratamiento de datos personales, consulta la página %{privacy_link}."
|
||||||
|
privacy_link_text: Privacidad
|
||||||
|
|||||||
@@ -195,3 +195,18 @@ fr:
|
|||||||
s6_body_html: "Pour exercer les droits prévus par le RGPD (accès, effacement, opposition, retrait du consentement), écrivez à %{email_link}. Détails dans le %{privacy_doc_link}."
|
s6_body_html: "Pour exercer les droits prévus par le RGPD (accès, effacement, opposition, retrait du consentement), écrivez à %{email_link}. Détails dans le %{privacy_doc_link}."
|
||||||
s6_privacy_doc_link_text: document de confidentialité
|
s6_privacy_doc_link_text: document de confidentialité
|
||||||
manage_button: Gérer les préférences de cookies
|
manage_button: Gérer les préférences de cookies
|
||||||
|
support:
|
||||||
|
title: Support Match Live TV
|
||||||
|
meta_description: "Assistance Match Live TV : problèmes d'accès, directs, gestion d'équipe et coordonnées du support."
|
||||||
|
h1: Support Match Live TV
|
||||||
|
intro: "Besoin d'aide avec Match Live TV ? Pour les problèmes d'accès, la configuration des directs, la gestion des matchs ou l'utilisation de l'application, vous pouvez contacter notre support."
|
||||||
|
email_label: "E-mail du support :"
|
||||||
|
access_title: Problèmes d'accès
|
||||||
|
access_body: "Si vous ne parvenez pas à vous connecter, vérifiez les identifiants fournis par votre club sportif. Si le problème persiste, contactez le support."
|
||||||
|
live_title: Problèmes pendant un direct
|
||||||
|
live_body: "Vérifiez votre connexion Internet et réessayez de démarrer la diffusion depuis l'application. Si le problème persiste, contactez le support en indiquant l'appareil, la version de l'application et une brève description du problème."
|
||||||
|
team_title: Gestion de l'équipe
|
||||||
|
team_body: "Les comptes et les autorisations de diffusion sont gérés par le club sportif."
|
||||||
|
privacy_title: Confidentialité
|
||||||
|
privacy_body_html: "Pour les informations sur le traitement des données personnelles, consultez la page %{privacy_link}."
|
||||||
|
privacy_link_text: Confidentialité
|
||||||
|
|||||||
@@ -195,3 +195,18 @@ it:
|
|||||||
s6_body_html: "Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso) scrivi a %{email_link}. Dettagli nel %{privacy_doc_link}."
|
s6_body_html: "Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso) scrivi a %{email_link}. Dettagli nel %{privacy_doc_link}."
|
||||||
s6_privacy_doc_link_text: documento privacy
|
s6_privacy_doc_link_text: documento privacy
|
||||||
manage_button: Gestisci preferenze cookie
|
manage_button: Gestisci preferenze cookie
|
||||||
|
support:
|
||||||
|
title: Supporto Match Live TV
|
||||||
|
meta_description: "Assistenza Match Live TV: problemi di accesso, dirette live, gestione squadra e contatti del supporto."
|
||||||
|
h1: Supporto Match Live TV
|
||||||
|
intro: "Hai bisogno di assistenza con Match Live TV? Per problemi di accesso, configurazione delle dirette, gestione delle partite o utilizzo dell’app puoi contattare il nostro supporto."
|
||||||
|
email_label: "Email di supporto:"
|
||||||
|
access_title: Problemi di accesso
|
||||||
|
access_body: "Se non riesci ad accedere, verifica le credenziali ricevute dalla tua società sportiva. Se il problema persiste, contatta il supporto."
|
||||||
|
live_title: Problemi durante una diretta
|
||||||
|
live_body: "Verifica la connessione Internet e riprova ad avviare la trasmissione dall’app. Se il problema persiste, contatta il supporto indicando dispositivo, versione dell’app e una breve descrizione del problema."
|
||||||
|
team_title: Gestione della squadra
|
||||||
|
team_body: "Gli account e le autorizzazioni per la trasmissione sono gestiti dalla società sportiva."
|
||||||
|
privacy_title: Privacy
|
||||||
|
privacy_body_html: "Per informazioni sul trattamento dei dati personali consulta la pagina %{privacy_link}."
|
||||||
|
privacy_link_text: Privacy
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ de:
|
|||||||
save_password: Passwort aktualisieren
|
save_password: Passwort aktualisieren
|
||||||
common:
|
common:
|
||||||
privacy: Datenschutz
|
privacy: Datenschutz
|
||||||
|
support: Support
|
||||||
cookies: Cookies
|
cookies: Cookies
|
||||||
terms: AGB
|
terms: AGB
|
||||||
pricing: Preise
|
pricing: Preise
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ en:
|
|||||||
save_password: Update password
|
save_password: Update password
|
||||||
common:
|
common:
|
||||||
privacy: Privacy
|
privacy: Privacy
|
||||||
|
support: Support
|
||||||
cookies: Cookies
|
cookies: Cookies
|
||||||
terms: Terms
|
terms: Terms
|
||||||
pricing: Pricing
|
pricing: Pricing
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ es:
|
|||||||
save_password: Actualizar contraseña
|
save_password: Actualizar contraseña
|
||||||
common:
|
common:
|
||||||
privacy: Privacidad
|
privacy: Privacidad
|
||||||
|
support: Soporte
|
||||||
cookies: Cookies
|
cookies: Cookies
|
||||||
terms: Términos
|
terms: Términos
|
||||||
pricing: Precios
|
pricing: Precios
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ fr:
|
|||||||
save_password: Mettre à jour le mot de passe
|
save_password: Mettre à jour le mot de passe
|
||||||
common:
|
common:
|
||||||
privacy: Confidentialité
|
privacy: Confidentialité
|
||||||
|
support: Support
|
||||||
cookies: Cookies
|
cookies: Cookies
|
||||||
terms: Conditions
|
terms: Conditions
|
||||||
pricing: Tarifs
|
pricing: Tarifs
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ it:
|
|||||||
save_password: Aggiorna password
|
save_password: Aggiorna password
|
||||||
common:
|
common:
|
||||||
privacy: Privacy
|
privacy: Privacy
|
||||||
|
support: Supporto
|
||||||
cookies: Cookie
|
cookies: Cookie
|
||||||
terms: Termini
|
terms: Termini
|
||||||
pricing: Prezzi
|
pricing: Prezzi
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ Rails.application.routes.draw do
|
|||||||
patch :stop
|
patch :stop
|
||||||
patch :pause
|
patch :pause
|
||||||
patch :resume
|
patch :resume
|
||||||
|
patch :audio_mute
|
||||||
patch :score
|
patch :score
|
||||||
post :score_action
|
post :score_action
|
||||||
get :events
|
get :events
|
||||||
@@ -142,6 +143,7 @@ Rails.application.routes.draw do
|
|||||||
patch "regia/:token/score", to: "public/regia#score", as: :public_regia_score
|
patch "regia/:token/score", to: "public/regia#score", as: :public_regia_score
|
||||||
post "regia/:token/pause", to: "public/regia#pause", as: :public_regia_pause
|
post "regia/:token/pause", to: "public/regia#pause", as: :public_regia_pause
|
||||||
post "regia/:token/resume", to: "public/regia#resume", as: :public_regia_resume
|
post "regia/:token/resume", to: "public/regia#resume", as: :public_regia_resume
|
||||||
|
post "regia/:token/audio_mute", to: "public/regia#audio_mute", as: :public_regia_audio_mute
|
||||||
post "regia/:token/stop", to: "public/regia#stop", as: :public_regia_stop
|
post "regia/:token/stop", to: "public/regia#stop", as: :public_regia_stop
|
||||||
|
|
||||||
root to: "public/pages#home"
|
root to: "public/pages#home"
|
||||||
@@ -154,6 +156,7 @@ Rails.application.routes.draw do
|
|||||||
get "prezzi", to: "pages#pricing", as: :prezzi
|
get "prezzi", to: "pages#pricing", as: :prezzi
|
||||||
get "pricing", to: redirect("/prezzi")
|
get "pricing", to: redirect("/prezzi")
|
||||||
get "privacy", to: "pages#privacy", as: :privacy
|
get "privacy", to: "pages#privacy", as: :privacy
|
||||||
|
get "support", to: "pages#support", as: :support
|
||||||
get "cookie", to: "pages#cookies", as: :cookies
|
get "cookie", to: "pages#cookies", as: :cookies
|
||||||
get "cookies", to: redirect("/cookie")
|
get "cookies", to: redirect("/cookie")
|
||||||
get "termini", to: "pages#terms", as: :termini
|
get "termini", to: "pages#terms", as: :termini
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddAudioMutedToStreamSessions < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :stream_sessions, :audio_muted, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2026_06_12_120000) do
|
ActiveRecord::Schema[7.2].define(version: 2026_08_12_220000) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -280,6 +280,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_12_120000) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "regia_token_digest"
|
t.string "regia_token_digest"
|
||||||
t.datetime "regia_token_expires_at"
|
t.datetime "regia_token_expires_at"
|
||||||
|
t.boolean "audio_muted", default: false, null: false
|
||||||
t.index ["match_id"], name: "index_stream_sessions_on_match_id"
|
t.index ["match_id"], name: "index_stream_sessions_on_match_id"
|
||||||
t.index ["publish_token"], name: "index_stream_sessions_on_publish_token", unique: true
|
t.index ["publish_token"], name: "index_stream_sessions_on_publish_token", unique: true
|
||||||
t.index ["regia_token_digest"], name: "index_stream_sessions_on_regia_token_digest", unique: true
|
t.index ["regia_token_digest"], name: "index_stream_sessions_on_regia_token_digest", unique: true
|
||||||
|
|||||||
@@ -165,6 +165,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.regia-btn--outline.is-muted {
|
||||||
|
border-color: var(--regia-yellow);
|
||||||
|
color: var(--regia-yellow);
|
||||||
|
}
|
||||||
.regia-btn--danger {
|
.regia-btn--danger {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--regia-red);
|
border: 1px solid var(--regia-red);
|
||||||
|
|||||||
@@ -25,9 +25,12 @@
|
|||||||
partialsPrefix: "Parziali:",
|
partialsPrefix: "Parziali:",
|
||||||
resumed: "Diretta ripresa",
|
resumed: "Diretta ripresa",
|
||||||
pausedCover: "Diretta in pausa — copertina in onda",
|
pausedCover: "Diretta in pausa — copertina in onda",
|
||||||
|
muted: "Audio silenziato — YouTube non riceve il microfono",
|
||||||
|
unmuted: "Audio microfono riattivato",
|
||||||
closed: "Diretta chiusa",
|
closed: "Diretta chiusa",
|
||||||
resumeError: "Errore ripresa diretta",
|
resumeError: "Errore ripresa diretta",
|
||||||
pauseError: "Errore pausa diretta",
|
pauseError: "Errore pausa diretta",
|
||||||
|
muteError: "Errore silenziamento audio",
|
||||||
closeError: "Errore chiusura",
|
closeError: "Errore chiusura",
|
||||||
closeConfirm: "Chiudere definitivamente la diretta?",
|
closeConfirm: "Chiudere definitivamente la diretta?",
|
||||||
linkUnavailable: "Link non disponibile",
|
linkUnavailable: "Link non disponibile",
|
||||||
@@ -39,6 +42,8 @@
|
|||||||
streamEnded: "Diretta terminata",
|
streamEnded: "Diretta terminata",
|
||||||
resumeLabel: "Riprendi diretta",
|
resumeLabel: "Riprendi diretta",
|
||||||
pauseLabel: "Metti in pausa",
|
pauseLabel: "Metti in pausa",
|
||||||
|
muteLabel: "Silenzia audio",
|
||||||
|
unmuteLabel: "Riattiva audio",
|
||||||
endedBadge: "Terminata",
|
endedBadge: "Terminata",
|
||||||
pausedBadge: "In pausa",
|
pausedBadge: "In pausa",
|
||||||
liveBadge: "In onda",
|
liveBadge: "In onda",
|
||||||
@@ -76,6 +81,7 @@
|
|||||||
scoreUrl: root.dataset.scoreUrl,
|
scoreUrl: root.dataset.scoreUrl,
|
||||||
pauseUrl: root.dataset.pauseUrl,
|
pauseUrl: root.dataset.pauseUrl,
|
||||||
resumeUrl: root.dataset.resumeUrl,
|
resumeUrl: root.dataset.resumeUrl,
|
||||||
|
audioMuteUrl: root.dataset.audioMuteUrl,
|
||||||
stopUrl: root.dataset.stopUrl,
|
stopUrl: root.dataset.stopUrl,
|
||||||
cableUrl: root.dataset.cableUrl,
|
cableUrl: root.dataset.cableUrl,
|
||||||
hlsUrl: root.dataset.hlsUrl,
|
hlsUrl: root.dataset.hlsUrl,
|
||||||
@@ -103,6 +109,7 @@
|
|||||||
preview: document.getElementById("regia-preview"),
|
preview: document.getElementById("regia-preview"),
|
||||||
previewPlaceholder: document.getElementById("regia-preview-placeholder"),
|
previewPlaceholder: document.getElementById("regia-preview-placeholder"),
|
||||||
btnPause: document.getElementById("btn-pause"),
|
btnPause: document.getElementById("btn-pause"),
|
||||||
|
btnAudioMute: document.getElementById("btn-audio-mute"),
|
||||||
subtitle: document.getElementById("regia-subtitle")
|
subtitle: document.getElementById("regia-subtitle")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +118,7 @@
|
|||||||
let previewStarted = false;
|
let previewStarted = false;
|
||||||
let wasPausedPreview = false;
|
let wasPausedPreview = false;
|
||||||
let streamPaused = root.dataset.initialPaused === "true";
|
let streamPaused = root.dataset.initialPaused === "true";
|
||||||
|
let audioMuted = root.dataset.initialAudioMuted === "true";
|
||||||
|
|
||||||
function hlsUrlFresh() {
|
function hlsUrlFresh() {
|
||||||
const sep = cfg.hlsUrl.includes("?") ? "&" : "?";
|
const sep = cfg.hlsUrl.includes("?") ? "&" : "?";
|
||||||
@@ -246,8 +254,18 @@
|
|||||||
els.btnPause.textContent = paused ? I18N.resumeLabel : I18N.pauseLabel;
|
els.btnPause.textContent = paused ? I18N.resumeLabel : I18N.pauseLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncMuteButton(data) {
|
||||||
|
if (!els.btnAudioMute) return;
|
||||||
|
if (typeof data.audio_muted === "boolean") {
|
||||||
|
audioMuted = data.audio_muted;
|
||||||
|
}
|
||||||
|
els.btnAudioMute.textContent = audioMuted ? I18N.unmuteLabel : I18N.muteLabel;
|
||||||
|
els.btnAudioMute.classList.toggle("is-muted", audioMuted);
|
||||||
|
}
|
||||||
|
|
||||||
function setBadge(data) {
|
function setBadge(data) {
|
||||||
syncPauseButton(data);
|
syncPauseButton(data);
|
||||||
|
syncMuteButton(data);
|
||||||
if (data.stream_closed) {
|
if (data.stream_closed) {
|
||||||
els.badge.textContent = I18N.endedBadge;
|
els.badge.textContent = I18N.endedBadge;
|
||||||
els.badge.className = "regia-badge regia-badge--ended";
|
els.badge.className = "regia-badge regia-badge--ended";
|
||||||
@@ -458,6 +476,24 @@
|
|||||||
toast(wasPaused ? I18N.resumed : I18N.pausedCover);
|
toast(wasPaused ? I18N.resumed : I18N.pausedCover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleMuteAudio() {
|
||||||
|
if (!cfg.audioMuteUrl) return;
|
||||||
|
const nextMuted = !audioMuted;
|
||||||
|
const res = await fetch(cfg.audioMuteUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
|
body: JSON.stringify({ muted: nextMuted })
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.error || I18N.muteError);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
applyScorePayload(data);
|
||||||
|
setBadge(data);
|
||||||
|
toast(audioMuted ? I18N.muted : I18N.unmuted);
|
||||||
|
}
|
||||||
|
|
||||||
async function stopStream() {
|
async function stopStream() {
|
||||||
const ok = confirm(I18N.closeConfirm);
|
const ok = confirm(I18N.closeConfirm);
|
||||||
if (!ok) return null;
|
if (!ok) return null;
|
||||||
@@ -471,6 +507,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
els.btnPause?.addEventListener("click", () => togglePauseStream().catch((e) => toast(e.message)));
|
els.btnPause?.addEventListener("click", () => togglePauseStream().catch((e) => toast(e.message)));
|
||||||
|
els.btnAudioMute?.addEventListener("click", () => toggleMuteAudio().catch((e) => toast(e.message)));
|
||||||
document.getElementById("btn-stop")?.addEventListener("click", () => stopStream().catch((e) => toast(e.message)));
|
document.getElementById("btn-stop")?.addEventListener("click", () => stopStream().catch((e) => toast(e.message)));
|
||||||
|
|
||||||
async function shareLink(url, title, text) {
|
async function shareLink(url, title, text) {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe "Api::V1 stream session audio mute", type: :request do
|
||||||
|
let!(:user) { User.create!(email: "mute-api@test.it", name: "U", password: "Password123", role: "coach") }
|
||||||
|
let!(:club) { Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
|
let!(:owner) { club.club_memberships.create!(user: user, role: "owner") }
|
||||||
|
let!(:team) { club.teams.create!(name: "T", sport: "volleyball") }
|
||||||
|
let!(:match) { team.matches.create!(opponent_name: "Opp", sport: "volleyball") }
|
||||||
|
let!(:session) { StreamSession.create!(match: match, user: user, platform: "youtube", status: "live") }
|
||||||
|
|
||||||
|
def auth_headers
|
||||||
|
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
|
||||||
|
token = response.parsed_body["access_token"]
|
||||||
|
{ "Authorization" => "Bearer #{token}", "Content-Type" => "application/json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "include audio_muted nello show della sessione" do
|
||||||
|
get "/api/v1/sessions/#{session.id}", headers: auth_headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.parsed_body["audio_muted"]).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "silenzia e riattiva l'audio via PATCH" do
|
||||||
|
patch "/api/v1/sessions/#{session.id}/audio_mute",
|
||||||
|
params: { muted: true }.to_json,
|
||||||
|
headers: auth_headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.parsed_body["audio_muted"]).to eq(true)
|
||||||
|
expect(session.reload.audio_muted).to eq(true)
|
||||||
|
|
||||||
|
patch "/api/v1/sessions/#{session.id}/audio_mute",
|
||||||
|
params: { muted: false }.to_json,
|
||||||
|
headers: auth_headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.parsed_body["audio_muted"]).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe "Public regia", type: :request do
|
RSpec.describe "Public regia", type: :request do
|
||||||
let!(:user) { User.create!(email: "regia@test.it", name: "U", password: "password123", role: "coach") }
|
let!(:user) { User.create!(email: "regia@test.it", name: "U", password: "Password123", role: "coach") }
|
||||||
let!(:club) { Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
let!(:club) { Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
let!(:team) { club.teams.create!(name: "T", sport: "volleyball") }
|
let!(:team) { club.teams.create!(name: "T", sport: "volleyball") }
|
||||||
let!(:match) { team.matches.create!(opponent_name: "Opp", sport: "volleyball") }
|
let!(:match) { team.matches.create!(opponent_name: "Opp", sport: "volleyball") }
|
||||||
@@ -148,6 +148,29 @@ RSpec.describe "Public regia", type: :request do
|
|||||||
expect(session.reload.status).to eq("connecting")
|
expect(session.reload.status).to eq("connecting")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "silenzia e riattiva l'audio della diretta" do
|
||||||
|
token = Sessions::RegiaAccess.new(session).issue_token!
|
||||||
|
expect(session.audio_muted).to eq(false)
|
||||||
|
|
||||||
|
post public_regia_audio_mute_path(token), params: { muted: true }, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.parsed_body["audio_muted"]).to eq(true)
|
||||||
|
expect(session.reload.audio_muted).to eq(true)
|
||||||
|
|
||||||
|
post public_regia_audio_mute_path(token), params: { muted: false }, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.parsed_body["audio_muted"]).to eq(false)
|
||||||
|
expect(session.reload.audio_muted).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "mostra il pulsante mute sulla pagina regia" do
|
||||||
|
token = Sessions::RegiaAccess.new(session).issue_token!
|
||||||
|
get public_regia_path(token)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("Silenzia audio")
|
||||||
|
expect(response.body).to include("btn-audio-mute")
|
||||||
|
end
|
||||||
|
|
||||||
it "emette token valido anche oltre le 8 ore dalla partenza" do
|
it "emette token valido anche oltre le 8 ore dalla partenza" do
|
||||||
session.update!(started_at: 9.hours.ago)
|
session.update!(started_at: 9.hours.ago)
|
||||||
token = Sessions::RegiaAccess.new(session).issue_token!
|
token = Sessions::RegiaAccess.new(session).issue_token!
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe "Public support page", type: :request do
|
||||||
|
it "è pubblica, indicizzabile e mostra l'email di supporto configurata" do
|
||||||
|
get public_support_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include(MatchLiveTv.support_email)
|
||||||
|
expect(response.body).to include("mailto:#{MatchLiveTv.support_email}")
|
||||||
|
expect(response.body).to include(I18n.t("legal.support.h1", locale: :it))
|
||||||
|
expect(response.body).to include('rel="canonical"')
|
||||||
|
expect(response.body).to include(public_support_path)
|
||||||
|
expect(response.body).to include(public_privacy_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "non espone CTA o link commerciali (App Store Review)" do
|
||||||
|
get public_support_path
|
||||||
|
body = response.body
|
||||||
|
|
||||||
|
expect(body).not_to include(public_prezzi_path)
|
||||||
|
expect(body).not_to include(public_signup_path)
|
||||||
|
expect(body).not_to include('href="/prezzi"')
|
||||||
|
expect(body).not_to include('href="/signup"')
|
||||||
|
expect(body).not_to include(I18n.t("nav.signup", locale: :it))
|
||||||
|
expect(body).not_to include(I18n.t("nav.pricing", locale: :it))
|
||||||
|
expect(body).not_to include(I18n.t("common.pricing", locale: :it))
|
||||||
|
expect(body).not_to match(/\bPremium Light\b/i)
|
||||||
|
expect(body).not_to match(/\bPremium Full\b/i)
|
||||||
|
expect(body).not_to match(/\bpiano Free\b/i)
|
||||||
|
expect(body).not_to match(/\bAbbonati\b/i)
|
||||||
|
expect(body).not_to match(/\bAcquista\b/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
"it" => "Supporto Match Live TV",
|
||||||
|
"en" => "Match Live TV Support",
|
||||||
|
"fr" => "Support Match Live TV",
|
||||||
|
"de" => "Match Live TV Support",
|
||||||
|
"es" => "Soporte Match Live TV"
|
||||||
|
}.each do |locale, heading|
|
||||||
|
it "renderizza correttamente in #{locale} senza translation missing" do
|
||||||
|
cookies[:mltv_locale] = locale
|
||||||
|
get public_support_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include(heading)
|
||||||
|
expect(response.body).not_to include("translation missing")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "include /support nella sitemap" do
|
||||||
|
get "/sitemap.xml"
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include("#{MatchLiveTv.app_public_url.chomp('/')}/support")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe Sessions::SetAudioMute do
|
||||||
|
let!(:user) { User.create!(email: "mute@test.it", name: "U", password: "Password123", role: "coach") }
|
||||||
|
let!(:club) { Club.create!(name: "C", sport: "volleyball", primary_color: "#e53935", secondary_color: "#ffffff") }
|
||||||
|
let!(:team) { club.teams.create!(name: "T", sport: "volleyball") }
|
||||||
|
let!(:match) { team.matches.create!(opponent_name: "Opp", sport: "volleyball") }
|
||||||
|
let!(:session) { StreamSession.create!(match: match, user: user, platform: "matchlivetv", status: "live") }
|
||||||
|
|
||||||
|
it "attiva il mute e persiste lo stato" do
|
||||||
|
described_class.new(session, muted: true).call
|
||||||
|
expect(session.reload.audio_muted).to eq(true)
|
||||||
|
expect(session.stream_events.order(:occurred_at).last.event_type).to eq("audio_muted")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "disattiva il mute" do
|
||||||
|
session.update!(audio_muted: true)
|
||||||
|
described_class.new(session, muted: false).call
|
||||||
|
expect(session.reload.audio_muted).to eq(false)
|
||||||
|
expect(session.stream_events.order(:occurred_at).last.event_type).to eq("audio_unmuted")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "è idempotente se lo stato è già quello richiesto" do
|
||||||
|
described_class.new(session, muted: false).call
|
||||||
|
expect(session.stream_events.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -38,10 +38,10 @@ Solo se la società vuole il **proprio** canale invece di Match Live TV:
|
|||||||
|
|
||||||
Nella home app (**Partite**):
|
Nella home app (**Partite**):
|
||||||
|
|
||||||
- **Partita programmata** — scegli dal calendario una gara già inserita (sito o app)
|
|
||||||
- **Nuova partita** — programma data/ora oppure **Avvia subito** senza orario
|
- **Nuova partita** — programma data/ora oppure **Avvia subito** senza orario
|
||||||
|
- Oppure tocca una gara già in calendario (sito o app)
|
||||||
|
|
||||||
Poi tocca la card o conferma dal foglio: si apre il wizard (Partita → Trasmissione → …).
|
Poi conferma dal foglio o tocca la card: si apre il wizard (Partita → Trasmissione → …).
|
||||||
|
|
||||||
## 4. Avvia diretta
|
## 4. Avvia diretta
|
||||||
|
|
||||||
|
|||||||
@@ -399,7 +399,8 @@ cd infra && cp .env.example .env && docker compose up -d --build
|
|||||||
| [`infrastructure/SERVER_DEPLOYMENT.md`](infrastructure/SERVER_DEPLOYMENT.md) | Bootstrap server, NPM, cron, backup |
|
| [`infrastructure/SERVER_DEPLOYMENT.md`](infrastructure/SERVER_DEPLOYMENT.md) | Bootstrap server, NPM, cron, backup |
|
||||||
| [`infrastructure/HETZNER_CLOUD_RELAY_OVERFLOW.md`](infrastructure/HETZNER_CLOUD_RELAY_OVERFLOW.md) | Piano overflow relay YouTube su Hetzner Cloud (picchi) |
|
| [`infrastructure/HETZNER_CLOUD_RELAY_OVERFLOW.md`](infrastructure/HETZNER_CLOUD_RELAY_OVERFLOW.md) | Piano overflow relay YouTube su Hetzner Cloud (picchi) |
|
||||||
| [`ANDROID_APP_LINKS.md`](ANDROID_APP_LINKS.md) | Digital Asset Links / deep link Play (`assetlinks.json`) |
|
| [`ANDROID_APP_LINKS.md`](ANDROID_APP_LINKS.md) | Digital Asset Links / deep link Play (`assetlinks.json`) |
|
||||||
| [`LIVE_STREAMING.md`](LIVE_STREAMING.md) | MediaMTX, pausa, HLS, ruolo ffmpeg |
|
| [`LIVE_STREAMING.md`](LIVE_STREAMING.md) | MediaMTX, pausa, mute audio, HLS, ruolo ffmpeg |
|
||||||
|
| [`IOS_STREAM_AUDIO_MUTE.md`](IOS_STREAM_AUDIO_MUTE.md) | Mute microfono iOS (allineamento Mac) |
|
||||||
| [`REPLAY_MODULE.md`](REPLAY_MODULE.md) | Garage, retention, YouTube VOD |
|
| [`REPLAY_MODULE.md`](REPLAY_MODULE.md) | Garage, retention, YouTube VOD |
|
||||||
| [`OPS_MONITORING.md`](OPS_MONITORING.md) | ntfy, variabili ops, troubleshooting |
|
| [`OPS_MONITORING.md`](OPS_MONITORING.md) | ntfy, variabili ops, troubleshooting |
|
||||||
| [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md) | Tabelloni e overlay (app nativa) |
|
| [`TABELLONI_E_OVERLAY.md`](TABELLONI_E_OVERLAY.md) | Tabelloni e overlay (app nativa) |
|
||||||
@@ -416,7 +417,7 @@ cd infra && cp .env.example .env && docker compose up -d --build
|
|||||||
| 2026-06 | Replay: redirect 302 a `/media/` → Garage (fuori da Puma) |
|
| 2026-06 | Replay: redirect 302 a `/media/` → Garage (fuori da Puma) |
|
||||||
| 2026-06 | HLS live: edge → MediaMTX (fuori da Puma) |
|
| 2026-06 | HLS live: edge → MediaMTX (fuori da Puma) |
|
||||||
| 2026-06 | Ops: check `http_rails` + `http_public` separati, latenza p95 `UpLatencyTracker` |
|
| 2026-06 | Ops: check `http_rails` + `http_public` separati, latenza p95 `UpLatencyTracker` |
|
||||||
| 2026-06 | `RAILS_MAX_THREADS=5`; relay YouTube da MediaMTX (non via Puma) |
|
| 2026-08 | Mute microfono in diretta (`audio_muted`) per evitare claim YouTube sulla musica in palestra |
|
||||||
| 2026-06 | Cleanup path MediaMTX orfani (`Mediamtx::CleanupOrphanPaths`); delete path sempre a fine diretta |
|
| 2026-06 | Cleanup path MediaMTX orfani (`Mediamtx::CleanupOrphanPaths`); delete path sempre a fine diretta |
|
||||||
| 2026-07 | Rimosso overlay server (`OverlayRelay`); tabellone bruciato in app; HLS su path camera (non `*_air`); `YoutubeRelay` = copy video + AAC |
|
| 2026-07 | Rimosso overlay server (`OverlayRelay`); tabellone bruciato in app; HLS su path camera (non `*_air`); `YoutubeRelay` = copy video + AAC |
|
||||||
| 2026-08 | `YoutubeRelay` = remux copy video+audio (niente ricodifica AAC); profilo app/slate AAC 48k mono |
|
| 2026-08 | `YoutubeRelay` = remux copy video+audio (niente ricodifica AAC); profilo app/slate AAC 48k mono |
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# Gap Android → iOS: allineamento app nativa
|
# Gap Android → iOS: allineamento app nativa
|
||||||
|
|
||||||
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android `2.0.5-native`**.
|
Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android**.
|
||||||
|
|
||||||
**Aggiornato:** 24 luglio 2026
|
**Aggiornato:** 12 agosto 2026
|
||||||
**Riferimento Android (produzione / telefono / Play):** `2.0.5-native` (`versionCode` **26**), API `https://www.matchlivetv.it`
|
**Riferimento Android (produzione / telefono / Play):** `2.0.10-native` (`versionCode` **31**), API `https://www.matchlivetv.it`
|
||||||
**Stato iOS attuale:** marketing `2.0.5`, build `26` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast)
|
**Stato iOS attuale:** marketing `2.0.10`, build `31` — i18n **allineato** (Login, hub, sheet/dialog, wizard, broadcast, account/forgot)
|
||||||
|
|
||||||
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione, **senza** i bug di sessione/crash già risolti su Android 2.0.5.
|
**Nuovo (branch `feature/stream-audio-mute`):** mute microfono in diretta. Codice iOS già nel branch; verifica su Mac in [`IOS_STREAM_AUDIO_MUTE.md`](IOS_STREAM_AUDIO_MUTE.md).
|
||||||
|
|
||||||
|
Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast, account) + bump versione, **senza** i bug di sessione/crash già risolti su Android.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS.
|
|||||||
| Broadcast + score dialog | Sì | `broadcast.*` / `score.*` | **Fatto** |
|
| Broadcast + score dialog | Sì | `broadcast.*` / `score.*` | **Fatto** |
|
||||||
| Catalogo stringhe | `values*` (~230) | `AppLanguage.swift` L10n (~225, no FGS) | **Fatto** |
|
| Catalogo stringhe | `values*` (~230) | `AppLanguage.swift` L10n (~225, no FGS) | **Fatto** |
|
||||||
| Logout UI | Icona | Icona SF Symbol | **Fatto** |
|
| Logout UI | Icona | Icona SF Symbol | **Fatto** |
|
||||||
| Versione | `2.0.5-native` / **26** | `2.0.5` / **26** | **Fatto** |
|
| Versione | `2.0.10-native` / **31** | `2.0.10` / **31** | **Fatto** |
|
||||||
| RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK |
|
| RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK |
|
||||||
|
|
||||||
### Residui fuori scope Android (opzionali)
|
### Residui fuori scope Android (opzionali)
|
||||||
@@ -141,7 +143,7 @@ xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \
|
|||||||
|
|
||||||
Nessuna modifica server richiesta.
|
Nessuna modifica server richiesta.
|
||||||
|
|
||||||
Versione store target: **`2.0.5` / build `26`** (parità con Android `2.0.5-native` / versionCode `26`).
|
Versione store target: **`2.0.10` / build `31`** (parità con Android `2.0.10-native` / versionCode `31`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
Documento operativo per **Cursor su Mac**: allineare l’app iOS a backend/Android dopo il ramo `feature/password-policy` (merge su `main`).
|
Documento operativo per **Cursor su Mac**: allineare l’app iOS a backend/Android dopo il ramo `feature/password-policy` (merge su `main`).
|
||||||
|
|
||||||
**Aggiornato:** 2026-08-08
|
**Aggiornato:** 2026-08-08
|
||||||
**Android di riferimento (testato su emulatore → API locale):** `2.0.10-native` (`versionCode` **31**)
|
**Android di riferimento:** `2.0.10-native` (`versionCode` **31**)
|
||||||
**iOS oggi:** marketing `2.0.5` / build `26`
|
**iOS (allineato):** marketing `2.0.10` / build `31`
|
||||||
**API produzione:** `https://www.matchlivetv.it`
|
**API produzione:** `https://www.matchlivetv.it`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -51,86 +51,38 @@ Chiavi rilevanti (`backend/config/locales/app.{it,en,fr,de,es}.yml`):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stato iOS vs Android (dopo il merge di questo branch)
|
## Stato iOS vs Android
|
||||||
|
|
||||||
| Area | Android 2.0.10 | iOS (repo dopo merge) | Da fare su Mac |
|
| Area | Android 2.0.10 | iOS | Stato |
|
||||||
|------|----------------|------------------------|----------------|
|
|------|----------------|-----|-------|
|
||||||
| Header `Accept-Language` su tutte le request API | `AppContainer` OkHttp interceptor | `MatchLiveAPI.request` + `multipartPatch` | **Verificare** in debug che parta su login/account/password; se manca su altri helper HTTP, allineare |
|
| Header `Accept-Language` su tutte le request API | OkHttp interceptor | `MatchLiveAPI.request` + `multipartPatch` | **OK** |
|
||||||
| Hint UI nuova password | `account_new_password` in `values*` (5 lingue) | `account.new.password` in `AppLanguage.swift` (già allineato al testo policy) | OK — rivedi solo se cambi copy |
|
| Hint UI nuova password | `account_new_password` | `account.new.password` | **OK** |
|
||||||
| Schermata Account | Mostra `error` dal body HTTP | `AccountScreen` + `APIError` estrae `error`/`message` | **Test manuale** (vedi sotto) |
|
| Schermata Account | AccountScreen | AccountScreen | **OK** |
|
||||||
| Parsing errori API | Regex su `"error"` | `APIError.friendlyHttpMessage` | OK |
|
| Forgot password | ForgotPasswordScreen | ForgotPasswordScreen | **OK** |
|
||||||
| Client-side pre-check complessità | No (si affida al server) | No | Opzionale: stessa regola di `PasswordComplexity` prima della PATCH |
|
| Parsing errori API | body `error` | `APIError.friendlyHttpMessage` | **OK** |
|
||||||
| Versione App Store | `2.0.10-native` / 31 | `2.0.5` / 26 | **Bump** marketing + build prima del submit TestFlight/App Store |
|
| Client-side pre-check complessità | No | No | N/A (come Android) |
|
||||||
| Signup in-app | Non è il flusso principale | Nessuna UI register dedicata | N/A (signup resta web) |
|
| Versione store | `2.0.10-native` / **31** | `2.0.10` / **31** | **OK** |
|
||||||
|
| Signup in-app | Non principale | Nessuna UI register | N/A (signup web) |
|
||||||
|
|
||||||
### File già toccati / da tenere
|
### File iOS
|
||||||
|
|
||||||
```
|
```
|
||||||
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # Accept-Language
|
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift
|
||||||
native/ios/MatchLiveTv/Core/AppLanguage.swift # hint account.new.password (5 lingue)
|
native/ios/MatchLiveTv/Core/AppLanguage.swift
|
||||||
native/ios/MatchLiveTv/UI/Account/AccountScreen.swift
|
native/ios/MatchLiveTv/UI/Account/AccountScreen.swift
|
||||||
native/ios/MatchLiveTv/Core/UserFacingError.swift
|
native/ios/MatchLiveTv/UI/Login/ForgotPasswordScreen.swift
|
||||||
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # enum APIError
|
native/ios/MatchLiveTv/Resources/Info.plist
|
||||||
|
native/ios/generate_xcodeproj.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Riferimento Android:
|
|
||||||
|
|
||||||
```
|
|
||||||
native/android/.../data/AppContainer.kt
|
|
||||||
native/android/.../ui/account/AccountScreen.kt
|
|
||||||
native/android/app/src/main/res/values*/strings.xml # account_new_password
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Checklist operativa su Mac
|
|
||||||
|
|
||||||
1. **Pull** `main` (dopo merge/push da Linux).
|
|
||||||
2. Apri `native/ios/MatchLiveTv.xcodeproj`.
|
|
||||||
3. Build debug con `API_BASE_URL=https://www.matchlivetv.it` (o locale se hai tunnel).
|
|
||||||
4. **Test Account** (credenziali demo se disponibili, altrimenti account reale di test):
|
|
||||||
- password debole (`password`) → messaggio *too_weak* nella lingua UI
|
|
||||||
- password = corrente → *same_as_current*
|
|
||||||
- conferma diversa → *mismatch*
|
|
||||||
- password valida nuova → successo; login successivo ok
|
|
||||||
5. Cambia lingua app (globo) e ripeti un errore: il testo API deve seguire `Accept-Language` / lingua scelta.
|
|
||||||
6. **Bump versione** allineata ad Android se rilasci store: es. marketing `2.0.10`, build `≥ 31`.
|
|
||||||
7. (Opzionale) Unit test Swift che replica `PasswordComplexity.strong_enough?` se aggiungi validazione client.
|
|
||||||
|
|
||||||
### Comandi utili
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone/pull
|
|
||||||
git checkout main && git pull
|
|
||||||
|
|
||||||
# Release iOS (script repo)
|
|
||||||
./scripts/build_ios_release.sh
|
|
||||||
# oppure API staging:
|
|
||||||
# API_BASE_URL=https://www.matchlivetv.it ./scripts/build_ios_release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cosa non serve rifare su iOS
|
|
||||||
|
|
||||||
- Fix CSS/menu mobile web, tabelle scroll, logo nel drawer: **solo sito**.
|
|
||||||
- Redirect `GET /account/password` → `/account`: **solo web**.
|
|
||||||
- Seed password / concern Rails: già lato server.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Retrocompatibilità (per release iOS)
|
|
||||||
|
|
||||||
- App iOS **vecchie** (senza `Accept-Language`): continuano a funzionare; messaggi API nella locale default del server.
|
|
||||||
- Login con password già esistenti: invariato.
|
|
||||||
- Solo **impostazione/cambio** password debole viene rifiutato (stesso JSON `error`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Criterio “fatto”
|
## Criterio “fatto”
|
||||||
|
|
||||||
- [ ] `Accept-Language` presente su login, me, account, change password, forgot (Charles/Proxyman o log)
|
- [x] `Accept-Language` su login, me, account, change password, forgot (`MatchLiveAPI`)
|
||||||
- [ ] Errori password in it/en/fr/de/es coerenti col backend
|
- [x] Hint campo nuova password parla di “min. 8 / 3 tipi”
|
||||||
- [ ] Hint campo nuova password parla di “min. 8 / 3 tipi”
|
- [x] Catalogo stringhe account/forgot in it/en/fr/de/es
|
||||||
- [ ] Versione bumpata se lo store release è in scope
|
- [x] Versione bumpata a `2.0.10` / `31`
|
||||||
- [ ] Nessuna regressione login / lista partite / broadcast
|
- [ ] QA manuale errori password in lingua UI (weak / same / mismatch / success)
|
||||||
|
|
||||||
Quando completo, aggiorna questo file con data + versione iOS rilasciata.
|
Quando rilasci su TestFlight/App Store, conferma la build `31`.
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# iOS — mute audio in diretta (allineamento da Mac)
|
||||||
|
|
||||||
|
Documento operativo per **Cursor su Mac**: verificare e, se serve, ritoccare l’app iOS dopo il ramo `feature/stream-audio-mute`.
|
||||||
|
|
||||||
|
**Aggiornato:** 2026-08-12
|
||||||
|
**Branch:** `feature/stream-audio-mute` (parte da `main`)
|
||||||
|
**Android di riferimento:** mute microfono in overlay + sync regia
|
||||||
|
**API produzione:** `https://www.matchlivetv.it`
|
||||||
|
|
||||||
|
Il codice iOS è **già nel branch** (engine, overlay, API, Cable, L10n). Su Mac va **compilato, testato su device** e, se HaishinKit 2.2.5 rifiuta un parametro, adattato come sotto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Perché esiste
|
||||||
|
|
||||||
|
In palestra la musica di sottofondo finisce nel microfono del telefono. Il relay YouTube fa `-c:a copy`, quindi YouTube sente quella musica e può reclamare il copyright.
|
||||||
|
|
||||||
|
Il mute **non ferma** lo stream: continua video + overlay + AAC, ma i sample audio sono silenzio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contratto API / Cable (già in backend)
|
||||||
|
|
||||||
|
| Pezzo | Dettaglio |
|
||||||
|
|-------|-----------|
|
||||||
|
| Campo sessione | `audio_muted` (boolean, default `false`) in JSON sessione e `GET/POST /regia/:token/status.json` |
|
||||||
|
| REST app | `PATCH /api/v1/sessions/:id/audio_mute` body `{ "muted": true \| false }` |
|
||||||
|
| REST regia | `POST /regia/:token/audio_mute` body `{ "muted": true \| false }` (senza body: toggle) |
|
||||||
|
| Cable command | `{ "type": "command", "action": "mute_audio" \| "unmute_audio", "muted": true \| false }` |
|
||||||
|
| Cable event | `{ "type": "stream_event", "event": "audio_muted", "muted": true \| false }` |
|
||||||
|
|
||||||
|
Idempotente: se lo stato è già quello richiesto, il backend non reinvia eventi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File iOS già modificati
|
||||||
|
|
||||||
|
```
|
||||||
|
native/ios/MatchLiveTv/Streaming/BroadcastModels.swift # BroadcastMetrics.audioMuted
|
||||||
|
native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift # setAudioMuted + AudioMixerSettings.isMuted
|
||||||
|
native/ios/MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift # setAudioMuted
|
||||||
|
native/ios/MatchLiveTv/Domain/Models.swift # StreamSession.audioMuted + withAudioMuted
|
||||||
|
native/ios/MatchLiveTv/Data/API/ApiDtos.swift # audioMuted + AudioMuteRequest
|
||||||
|
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # setAudioMute
|
||||||
|
native/ios/MatchLiveTv/Data/Repository/SessionRepository.swift
|
||||||
|
native/ios/MatchLiveTv/Data/Cable/SessionCableService.swift # onAudioMute + AudioMuteCommandLogic
|
||||||
|
native/ios/MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift
|
||||||
|
native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift
|
||||||
|
native/ios/MatchLiveTv/Core/AppLanguage.swift # IT/EN/FR/DE/ES
|
||||||
|
native/ios/MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Engine HaishinKit 2.2.5 (punto da verificare su Mac)
|
||||||
|
|
||||||
|
Implementazione attesa in `LiveBroadcastEngine`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func setAudioMuted(_ muted: Bool) async {
|
||||||
|
audioMuted = muted
|
||||||
|
await applyAudioMute()
|
||||||
|
metrics.audioMuted = muted
|
||||||
|
emitMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyAudioMute() async {
|
||||||
|
guard pipelineConfigured else { return }
|
||||||
|
var settings = await mixer.audioMixerSettings
|
||||||
|
settings.isMuted = audioMuted
|
||||||
|
var track = settings.tracks[0] ?? AudioMixerTrackSettings(downmix: true, channelMap: [0])
|
||||||
|
track.isMuted = audioMuted
|
||||||
|
settings.tracks[0] = track
|
||||||
|
try? await mixer.setAudioMixerSettings(settings)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `configurePipeline` i settings iniziali passano `isMuted: audioMuted` sia sul mixer sia sulla track 0.
|
||||||
|
|
||||||
|
**Se il build fallisce:**
|
||||||
|
|
||||||
|
1. `AudioMixerSettings` potrebbe non avere `isMuted` nel memberwise usato oggi — in quel caso muta solo `settings.tracks[0].isMuted`.
|
||||||
|
2. `setAudioMixerSettings` è `async throws` nel codice attuale; se la signature è sync, togli `try await`.
|
||||||
|
3. Non staccare il microfono (`attachAudio(nil)`): YouTube/MediaMTX vogliono AAC continuo. Mute = silenzio, non assenza di traccia.
|
||||||
|
|
||||||
|
Dopo `prepareBroadcast` / `resumeBroadcast` la schermata richiama `setAudioMuted(session.audioMuted)` per riallineare lo stato persistito.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI overlay
|
||||||
|
|
||||||
|
Toolbar destra (come Android), sotto pausa:
|
||||||
|
|
||||||
|
| Stato | SF Symbol | Chiave L10n |
|
||||||
|
|-------|-----------|-------------|
|
||||||
|
| Audio on | `speaker.wave.2.fill` | `broadcast.mute.cd` |
|
||||||
|
| Audio off | `speaker.slash.fill` | `broadcast.unmute.cd` |
|
||||||
|
|
||||||
|
Bottone `highlighted` quando mutato (stesso verde della pausa). Pannello telemetria: riga `broadcast.audio.muted.label` se mutato.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chiavi L10n (già aggiunte, 5 lingue)
|
||||||
|
|
||||||
|
| Android | iOS |
|
||||||
|
|---------|-----|
|
||||||
|
| `broadcast_mute_cd` | `broadcast.mute.cd` |
|
||||||
|
| `broadcast_unmute_cd` | `broadcast.unmute.cd` |
|
||||||
|
| `broadcast_audio_muted_label` | `broadcast.audio.muted.label` |
|
||||||
|
| `broadcast_snackbar_muted` | `broadcast.snackbar.muted` |
|
||||||
|
| `broadcast_snackbar_unmuted` | `broadcast.snackbar.unmuted` |
|
||||||
|
| `broadcast_snackbar_muted_remote` | `broadcast.snackbar.muted.remote` |
|
||||||
|
| `broadcast_snackbar_unmuted_remote` | `broadcast.snackbar.unmuted.remote` |
|
||||||
|
| `broadcast_snackbar_mute_error` | `broadcast.snackbar.mute.error` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test su Mac (acceptance)
|
||||||
|
|
||||||
|
1. `cd native/ios && python3 generate_xcodeproj.py`
|
||||||
|
2. Build simulatore / device.
|
||||||
|
3. Avvia una diretta (anche verso ambiente di staging).
|
||||||
|
4. Overlay: tap mute → icona speaker.slash, snackbar «Audio silenziato», telemetria «Audio off».
|
||||||
|
5. Apri la **regia** sullo stesso match → il pulsante deve dire «Riattiva audio».
|
||||||
|
6. Dalla regia riattiva → il telefono mostra speaker.wave e snackbar «dalla regia».
|
||||||
|
7. Mute, metti in **pausa**, riprendi: l’audio resta mutato (stato persistito).
|
||||||
|
8. YouTube / player HLS: video continua, audio silenzioso (non assente).
|
||||||
|
9. Unit test: `LiveBroadcastCoordinatorTests.testAudioMuteCommandFromCable`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd native/ios
|
||||||
|
python3 generate_xcodeproj.py
|
||||||
|
xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \
|
||||||
|
-destination 'generic/platform=iOS Simulator' \
|
||||||
|
-derivedDataPath build/DerivedData \
|
||||||
|
ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64 \
|
||||||
|
build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criterio “fatto”
|
||||||
|
|
||||||
|
- [ ] Build iOS senza errori HaishinKit
|
||||||
|
- [ ] Mute/unmute locale in overlay
|
||||||
|
- [ ] Sync bidirezionale con la regia via Cable + REST
|
||||||
|
- [ ] Mute sopravvive a pausa/ripresa
|
||||||
|
- [ ] AAC silenzioso in onda (niente drop della traccia audio)
|
||||||
|
- [ ] Stringhe IT/EN/FR/DE/ES visibili in overlay
|
||||||
+14
-1
@@ -1,6 +1,6 @@
|
|||||||
# Diretta live — architettura MediaMTX
|
# Diretta live — architettura MediaMTX
|
||||||
|
|
||||||
**Ultimo aggiornamento:** 2026-07-29
|
**Ultimo aggiornamento:** 2026-08-12
|
||||||
|
|
||||||
## Idea
|
## Idea
|
||||||
|
|
||||||
@@ -106,6 +106,19 @@ L’app mobile persiste il punteggio con `PATCH /api/v1/sessions/:id/score` (o `
|
|||||||
|
|
||||||
`GET /live/:id/status.json` resta per messaggi sotto il player e recovery HLS; `Cache-Control: no-store`.
|
`GET /live/:id/status.json` resta per messaggi sotto il player e recovery HLS; `Cache-Control: no-store`.
|
||||||
|
|
||||||
|
### Mute audio (copyright palestra / YouTube)
|
||||||
|
|
||||||
|
Se in palestra c’è musica di sottofondo, YouTube può reclamare il copyright sul flusso inoltrato (`-c:a copy`). L’operatore può **silenziare il microfono** senza interrompere video, overlay, HLS o relay:
|
||||||
|
|
||||||
|
1. Lo stato `audio_muted` è persistito su `stream_sessions` (default `false`).
|
||||||
|
2. API `PATCH /api/v1/sessions/:id/audio_mute` `{ "muted": true|false }` e `POST /regia/:token/audio_mute`.
|
||||||
|
3. Action Cable: `{ type: "command", action: "mute_audio"|"unmute_audio" }` e `{ type: "stream_event", event: "audio_muted", muted: true|false }`.
|
||||||
|
4. L’app **non toglie la traccia AAC**: azzera i sample PCM (silenzio). YouTube e MediaMTX continuano a ricevere audio 48 kHz mono.
|
||||||
|
|
||||||
|
Pulsante mute: overlay diretta (Android/iOS, accanto a pausa) e pagina regia.
|
||||||
|
|
||||||
|
Dettaglio implementazione iOS: [`IOS_STREAM_AUDIO_MUTE.md`](IOS_STREAM_AUDIO_MUTE.md).
|
||||||
|
|
||||||
## Rigenerare la slate
|
## Rigenerare la slate
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ YOUTUBE_MOCK_STREAM_KEY=mock-stream-key
|
|||||||
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
||||||
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
||||||
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
|
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
|
||||||
|
# Opzionale: email dedicata di supporto (default = info@matchlivetv.it)
|
||||||
|
# SUPPORT_CONTACT_EMAIL=info@matchlivetv.it
|
||||||
MAILER_FROM=Match Live TV <noreply@matchlivetv.it>
|
MAILER_FROM=Match Live TV <noreply@matchlivetv.it>
|
||||||
PASSWORD_RESET_EXPIRY_HOURS=2
|
PASSWORD_RESET_EXPIRY_HOURS=2
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ RAILS_LOG_LEVEL=info
|
|||||||
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
||||||
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
||||||
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
|
PRIVACY_CONTACT_EMAIL=privacy@matchlivetv.it
|
||||||
|
# Opzionale: email di supporto App Store (default = info@matchlivetv.it)
|
||||||
|
# SUPPORT_CONTACT_EMAIL=info@matchlivetv.it
|
||||||
PRIVACY_CONTROLLER_VAT=
|
PRIVACY_CONTROLLER_VAT=
|
||||||
|
|
||||||
# Email transazionali (reset password, inviti)
|
# Email transazionali (reset password, inviti)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ services:
|
|||||||
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
||||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||||
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
|
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
|
||||||
|
SUPPORT_CONTACT_EMAIL: ${SUPPORT_CONTACT_EMAIL:-}
|
||||||
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
||||||
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
||||||
PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-}
|
PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ services:
|
|||||||
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
||||||
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
||||||
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
|
PRIVACY_CONTACT_EMAIL: ${PRIVACY_CONTACT_EMAIL:-privacy@matchlivetv.it}
|
||||||
|
SUPPORT_CONTACT_EMAIL: ${SUPPORT_CONTACT_EMAIL:-}
|
||||||
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>}
|
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>}
|
||||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ data class StreamSessionDto(
|
|||||||
@Json(name = "target_fps") val targetFps: Int? = null,
|
@Json(name = "target_fps") val targetFps: Int? = null,
|
||||||
@Json(name = "quality_preset") val qualityPreset: String? = null,
|
@Json(name = "quality_preset") val qualityPreset: String? = null,
|
||||||
@Json(name = "started_at") val startedAt: String? = null,
|
@Json(name = "started_at") val startedAt: String? = null,
|
||||||
|
@Json(name = "audio_muted") val audioMuted: Boolean? = null,
|
||||||
val score: ScoreStateDto? = null,
|
val score: ScoreStateDto? = null,
|
||||||
) {
|
) {
|
||||||
fun toDomain() = StreamSession(
|
fun toDomain() = StreamSession(
|
||||||
@@ -203,6 +204,7 @@ data class StreamSessionDto(
|
|||||||
qualityPreset = qualityPreset ?: "720p_30_2.5mbps",
|
qualityPreset = qualityPreset ?: "720p_30_2.5mbps",
|
||||||
startedAt = startedAt,
|
startedAt = startedAt,
|
||||||
score = score?.toDomain(),
|
score = score?.toDomain(),
|
||||||
|
audioMuted = audioMuted ?: false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +369,10 @@ data class RegiaLinkResponse(
|
|||||||
@Json(name = "regia_url") val regiaUrl: String,
|
@Json(name = "regia_url") val regiaUrl: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class AudioMuteRequest(
|
||||||
|
val muted: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
data class CreateMatchBody(
|
data class CreateMatchBody(
|
||||||
@Json(name = "opponent_name") val opponentName: String,
|
@Json(name = "opponent_name") val opponentName: String,
|
||||||
val sport: String = "volleyball",
|
val sport: String = "volleyball",
|
||||||
|
|||||||
+6
@@ -117,6 +117,12 @@ interface MatchLiveApi {
|
|||||||
@PATCH("sessions/{sessionId}/resume")
|
@PATCH("sessions/{sessionId}/resume")
|
||||||
suspend fun resumeSession(@Path("sessionId") sessionId: String): StreamSessionDto
|
suspend fun resumeSession(@Path("sessionId") sessionId: String): StreamSessionDto
|
||||||
|
|
||||||
|
@PATCH("sessions/{sessionId}/audio_mute")
|
||||||
|
suspend fun setAudioMute(
|
||||||
|
@Path("sessionId") sessionId: String,
|
||||||
|
@Body body: AudioMuteRequest,
|
||||||
|
): StreamSessionDto
|
||||||
|
|
||||||
@POST("sessions/{sessionId}/network_test")
|
@POST("sessions/{sessionId}/network_test")
|
||||||
suspend fun networkTest(
|
suspend fun networkTest(
|
||||||
@Path("sessionId") sessionId: String,
|
@Path("sessionId") sessionId: String,
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.matchlivetv.match_live_tv.data.cable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpreta i payload ActionCable del mute audio.
|
||||||
|
* Usato da app e test: lo stream resta in onda, cambia solo il microfono.
|
||||||
|
*/
|
||||||
|
object AudioMuteCommands {
|
||||||
|
fun mutedFromPayload(
|
||||||
|
type: String?,
|
||||||
|
action: String? = null,
|
||||||
|
event: String? = null,
|
||||||
|
muted: Any? = null,
|
||||||
|
): Boolean? = when (type) {
|
||||||
|
"command" -> when (action) {
|
||||||
|
"mute_audio" -> true
|
||||||
|
"unmute_audio" -> false
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
"stream_event" -> if (event == "audio_muted") {
|
||||||
|
muted == true || muted == "true"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-8
@@ -33,6 +33,7 @@ class SessionCableService(
|
|||||||
var onPauseStream: (() -> Unit)? = null
|
var onPauseStream: (() -> Unit)? = null
|
||||||
var onResumeStream: (() -> Unit)? = null
|
var onResumeStream: (() -> Unit)? = null
|
||||||
var onEndStream: (() -> Unit)? = null
|
var onEndStream: (() -> Unit)? = null
|
||||||
|
var onAudioMute: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
fun connect(sessionId: String, accessToken: String, deviceRole: String = "camera") {
|
fun connect(sessionId: String, accessToken: String, deviceRole: String = "camera") {
|
||||||
intentionalDisconnect = false
|
intentionalDisconnect = false
|
||||||
@@ -105,15 +106,28 @@ class SessionCableService(
|
|||||||
val payload = unwrapCablePayload(data)
|
val payload = unwrapCablePayload(data)
|
||||||
when (payload["type"] as? String) {
|
when (payload["type"] as? String) {
|
||||||
"score_update" -> onScoreUpdate?.invoke(parseScore(payload))
|
"score_update" -> onScoreUpdate?.invoke(parseScore(payload))
|
||||||
"command" -> when (payload["action"] as? String) {
|
"command" -> {
|
||||||
"pause_stream" -> onPauseStream?.invoke()
|
AudioMuteCommands.mutedFromPayload(
|
||||||
"resume_stream" -> onResumeStream?.invoke()
|
type = "command",
|
||||||
"stop_stream" -> onEndStream?.invoke()
|
action = payload["action"] as? String,
|
||||||
|
)?.let { onAudioMute?.invoke(it) }
|
||||||
|
when (payload["action"] as? String) {
|
||||||
|
"pause_stream" -> onPauseStream?.invoke()
|
||||||
|
"resume_stream" -> onResumeStream?.invoke()
|
||||||
|
"stop_stream" -> onEndStream?.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"stream_event" -> when (payload["event"] as? String) {
|
"stream_event" -> {
|
||||||
"paused" -> onPauseStream?.invoke()
|
AudioMuteCommands.mutedFromPayload(
|
||||||
"resumed" -> onResumeStream?.invoke()
|
type = "stream_event",
|
||||||
"ended" -> onEndStream?.invoke()
|
event = payload["event"] as? String,
|
||||||
|
muted = payload["muted"],
|
||||||
|
)?.let { onAudioMute?.invoke(it) }
|
||||||
|
when (payload["event"] as? String) {
|
||||||
|
"paused" -> onPauseStream?.invoke()
|
||||||
|
"resumed" -> onResumeStream?.invoke()
|
||||||
|
"ended" -> onEndStream?.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -38,6 +38,9 @@ class SessionRepository(
|
|||||||
suspend fun resumeSession(sessionId: String): StreamSession =
|
suspend fun resumeSession(sessionId: String): StreamSession =
|
||||||
api.resumeSession(sessionId).toDomain()
|
api.resumeSession(sessionId).toDomain()
|
||||||
|
|
||||||
|
suspend fun setAudioMute(sessionId: String, muted: Boolean): StreamSession =
|
||||||
|
api.setAudioMute(sessionId, com.matchlivetv.match_live_tv.data.api.AudioMuteRequest(muted)).toDomain()
|
||||||
|
|
||||||
suspend fun submitNetworkTest(
|
suspend fun submitNetworkTest(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
downloadMbps: Double,
|
downloadMbps: Double,
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ data class StreamSession(
|
|||||||
val qualityPreset: String = "720p_30_2.5mbps",
|
val qualityPreset: String = "720p_30_2.5mbps",
|
||||||
val startedAt: String? = null,
|
val startedAt: String? = null,
|
||||||
val score: ScoreState? = null,
|
val score: ScoreState? = null,
|
||||||
|
val audioMuted: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val isLive: Boolean get() = status == "live" || status == "reconnecting"
|
val isLive: Boolean get() = status == "live" || status == "reconnecting"
|
||||||
|
|
||||||
|
|||||||
+1
@@ -39,4 +39,5 @@ data class BroadcastMetrics(
|
|||||||
val fps: Int = 0,
|
val fps: Int = 0,
|
||||||
val connected: Boolean = false,
|
val connected: Boolean = false,
|
||||||
val lastError: String? = null,
|
val lastError: String? = null,
|
||||||
|
val audioMuted: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
+4
@@ -33,6 +33,10 @@ class LiveBroadcastCoordinator(context: Context) {
|
|||||||
engine.updateOverlay(state)
|
engine.updateOverlay(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setAudioMuted(muted: Boolean) {
|
||||||
|
engine.setAudioMuted(muted)
|
||||||
|
}
|
||||||
|
|
||||||
fun startService() {
|
fun startService() {
|
||||||
val intent = LiveBroadcastService.startIntent(appContext)
|
val intent = LiveBroadcastService.startIntent(appContext)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|||||||
+32
@@ -8,6 +8,8 @@ import android.util.Log
|
|||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import com.pedro.common.ConnectChecker
|
import com.pedro.common.ConnectChecker
|
||||||
|
import com.pedro.encoder.input.sources.audio.MicrophoneSource
|
||||||
|
import com.pedro.encoder.input.sources.audio.MixAudioSource
|
||||||
import com.pedro.library.generic.GenericStream
|
import com.pedro.library.generic.GenericStream
|
||||||
import com.pedro.library.util.SensorRotationManager
|
import com.pedro.library.util.SensorRotationManager
|
||||||
import com.matchlivetv.match_live_tv.streaming.overlay.OverlayRenderer
|
import com.matchlivetv.match_live_tv.streaming.overlay.OverlayRenderer
|
||||||
@@ -42,6 +44,7 @@ class LiveBroadcastEngine(
|
|||||||
private val pausingIntentionally = AtomicBoolean(false)
|
private val pausingIntentionally = AtomicBoolean(false)
|
||||||
private val reconnectAttempts = AtomicInteger(0)
|
private val reconnectAttempts = AtomicInteger(0)
|
||||||
private val overlayRenderer = OverlayRenderer(appContext)
|
private val overlayRenderer = OverlayRenderer(appContext)
|
||||||
|
private val desiredAudioMuted = AtomicBoolean(false)
|
||||||
|
|
||||||
private val rebindRunnable = Runnable {
|
private val rebindRunnable = Runnable {
|
||||||
reattachPreviewIfNeeded()
|
reattachPreviewIfNeeded()
|
||||||
@@ -227,6 +230,7 @@ class LiveBroadcastEngine(
|
|||||||
currentBitrateKbps = 0
|
currentBitrateKbps = 0
|
||||||
currentFps = 0
|
currentFps = 0
|
||||||
reconnectAttempts.set(0)
|
reconnectAttempts.set(0)
|
||||||
|
desiredAudioMuted.set(false)
|
||||||
setPhase(BroadcastPhase.IDLE)
|
setPhase(BroadcastPhase.IDLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,6 +268,17 @@ class LiveBroadcastEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Silenzia il microfono senza togliere la traccia AAC (YouTube/MediaMTX richiedono audio continuo). */
|
||||||
|
fun setAudioMuted(muted: Boolean) {
|
||||||
|
mainHandler.post {
|
||||||
|
desiredAudioMuted.set(muted)
|
||||||
|
applyAudioMuteInternal()
|
||||||
|
emitMetrics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAudioMuted(): Boolean = desiredAudioMuted.get()
|
||||||
|
|
||||||
/** Adattamento termico: riduce bitrate/FPS senza fermare lo stream. */
|
/** Adattamento termico: riduce bitrate/FPS senza fermare lo stream. */
|
||||||
fun applyThermalProfile(videoBitrate: Int, fps: Int) {
|
fun applyThermalProfile(videoBitrate: Int, fps: Int) {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
@@ -340,6 +355,7 @@ class LiveBroadcastEngine(
|
|||||||
applyInitialOrientation()
|
applyInitialOrientation()
|
||||||
attachOverlayFilter(cfg)
|
attachOverlayFilter(cfg)
|
||||||
startSensor()
|
startSensor()
|
||||||
|
applyAudioMuteInternal()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,6 +530,7 @@ class LiveBroadcastEngine(
|
|||||||
},
|
},
|
||||||
connected = phase == BroadcastPhase.LIVE,
|
connected = phase == BroadcastPhase.LIVE,
|
||||||
lastError = error,
|
lastError = error,
|
||||||
|
audioMuted = desiredAudioMuted.get(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -579,6 +596,21 @@ class LiveBroadcastEngine(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyAudioMuteInternal() {
|
||||||
|
val muted = desiredAudioMuted.get()
|
||||||
|
val source = genericStream?.audioSource ?: return
|
||||||
|
runCatching {
|
||||||
|
when (source) {
|
||||||
|
is MicrophoneSource -> if (muted) source.mute() else source.unMute()
|
||||||
|
is MixAudioSource -> if (muted) source.muteMicrophone() else source.unMuteMicrophone()
|
||||||
|
else -> Log.w(TAG, "applyAudioMute: unsupported source ${source::class.java.simpleName}")
|
||||||
|
}
|
||||||
|
Log.i(TAG, "audio muted=$muted source=${source::class.java.simpleName}")
|
||||||
|
}.onFailure {
|
||||||
|
Log.w(TAG, "applyAudioMute failed: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun userFacingError(reason: String): String = when {
|
private fun userFacingError(reason: String): String = when {
|
||||||
reason.contains("broken pipe", ignoreCase = true) ->
|
reason.contains("broken pipe", ignoreCase = true) ->
|
||||||
"Connessione persa — riprova a riavviare la diretta"
|
"Connessione persa — riprova a riavviare la diretta"
|
||||||
|
|||||||
+24
@@ -32,6 +32,8 @@ import androidx.compose.material.icons.filled.Stop
|
|||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material.icons.filled.Videocam
|
import androidx.compose.material.icons.filled.Videocam
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.VolumeOff
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -100,6 +102,8 @@ fun BroadcastControlsOverlay(
|
|||||||
onCloseSet: (() -> Unit)? = null,
|
onCloseSet: (() -> Unit)? = null,
|
||||||
onAdvancePeriod: (() -> Unit)? = null,
|
onAdvancePeriod: (() -> Unit)? = null,
|
||||||
onPauseOrResume: () -> Unit,
|
onPauseOrResume: () -> Unit,
|
||||||
|
onToggleAudioMute: () -> Unit,
|
||||||
|
audioMuted: Boolean,
|
||||||
onTerminate: () -> Unit,
|
onTerminate: () -> Unit,
|
||||||
onShareLive: () -> Unit,
|
onShareLive: () -> Unit,
|
||||||
onShareRegia: () -> Unit,
|
onShareRegia: () -> Unit,
|
||||||
@@ -174,6 +178,7 @@ fun BroadcastControlsOverlay(
|
|||||||
bitrateKbps = bitrateKbps,
|
bitrateKbps = bitrateKbps,
|
||||||
networkType = networkType,
|
networkType = networkType,
|
||||||
deviceHealth = deviceHealth,
|
deviceHealth = deviceHealth,
|
||||||
|
audioMuted = audioMuted,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +232,16 @@ fun BroadcastControlsOverlay(
|
|||||||
onClick = onPauseOrResume,
|
onClick = onPauseOrResume,
|
||||||
highlighted = isPaused,
|
highlighted = isPaused,
|
||||||
)
|
)
|
||||||
|
SideIconButton(
|
||||||
|
icon = if (audioMuted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp,
|
||||||
|
contentDescription = if (audioMuted) {
|
||||||
|
stringResource(R.string.broadcast_unmute_cd)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.broadcast_mute_cd)
|
||||||
|
},
|
||||||
|
onClick = onToggleAudioMute,
|
||||||
|
highlighted = audioMuted,
|
||||||
|
)
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
icon = Icons.Default.Stop,
|
icon = Icons.Default.Stop,
|
||||||
contentDescription = stringResource(R.string.broadcast_terminate_cd),
|
contentDescription = stringResource(R.string.broadcast_terminate_cd),
|
||||||
@@ -287,6 +302,7 @@ private fun BroadcastTelemetryPanel(
|
|||||||
bitrateKbps: Long,
|
bitrateKbps: Long,
|
||||||
networkType: String,
|
networkType: String,
|
||||||
deviceHealth: DeviceHealthSnapshot,
|
deviceHealth: DeviceHealthSnapshot,
|
||||||
|
audioMuted: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val labelStyle = MaterialTheme.typography.labelSmall
|
val labelStyle = MaterialTheme.typography.labelSmall
|
||||||
@@ -326,6 +342,14 @@ private fun BroadcastTelemetryPanel(
|
|||||||
style = labelStyle,
|
style = labelStyle,
|
||||||
color = MatchColors.TextSecondary,
|
color = MatchColors.TextSecondary,
|
||||||
)
|
)
|
||||||
|
if (audioMuted) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.broadcast_audio_muted_label),
|
||||||
|
style = labelStyle,
|
||||||
|
color = MatchColors.AccentYellow,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
ThermalIndicator(state = deviceHealth.thermalState)
|
ThermalIndicator(state = deviceHealth.thermalState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
@@ -78,6 +78,7 @@ fun BroadcastScreen(
|
|||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
var pauseInFlight by remember { mutableStateOf(false) }
|
var pauseInFlight by remember { mutableStateOf(false) }
|
||||||
var resumeInFlight by remember { mutableStateOf(false) }
|
var resumeInFlight by remember { mutableStateOf(false) }
|
||||||
|
var muteInFlight by remember { mutableStateOf(false) }
|
||||||
var controlsVisible by remember { mutableStateOf(true) }
|
var controlsVisible by remember { mutableStateOf(true) }
|
||||||
var logoReady by remember { mutableStateOf(0) }
|
var logoReady by remember { mutableStateOf(0) }
|
||||||
var deviceHealth by remember { mutableStateOf(DeviceTelemetry.snapshot(context)) }
|
var deviceHealth by remember { mutableStateOf(DeviceTelemetry.snapshot(context)) }
|
||||||
@@ -145,6 +146,7 @@ fun BroadcastScreen(
|
|||||||
}
|
}
|
||||||
updated = container.sessionRepository.fetchSession(sessionId)
|
updated = container.sessionRepository.fetchSession(sessionId)
|
||||||
session = updated
|
session = updated
|
||||||
|
container.broadcastCoordinator.setAudioMuted(updated.audioMuted)
|
||||||
snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_resumed))
|
snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_resumed))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
snackbar.showSnackbar(
|
snackbar.showSnackbar(
|
||||||
@@ -174,6 +176,7 @@ fun BroadcastScreen(
|
|||||||
thermalManager.updateBaseline(cfg)
|
thermalManager.updateBaseline(cfg)
|
||||||
container.broadcastCoordinator.engine.resumeBroadcast(cfg)
|
container.broadcastCoordinator.engine.resumeBroadcast(cfg)
|
||||||
snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_resumed_remote))
|
snackbar.showSnackbar(context.getString(R.string.broadcast_snackbar_resumed_remote))
|
||||||
|
container.broadcastCoordinator.setAudioMuted(current.audioMuted)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
snackbar.showSnackbar(
|
snackbar.showSnackbar(
|
||||||
context.getString(R.string.broadcast_snackbar_resume_rtmp_error, e.message ?: genericError),
|
context.getString(R.string.broadcast_snackbar_resume_rtmp_error, e.message ?: genericError),
|
||||||
@@ -191,6 +194,40 @@ fun BroadcastScreen(
|
|||||||
onFinished()
|
onFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setAudioMuted(muted: Boolean, remote: Boolean) {
|
||||||
|
if (muteInFlight) return
|
||||||
|
if ((session?.audioMuted == true || metrics.audioMuted) == muted) {
|
||||||
|
container.broadcastCoordinator.setAudioMuted(muted)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
muteInFlight = true
|
||||||
|
try {
|
||||||
|
if (!remote) {
|
||||||
|
val updated = container.sessionRepository.setAudioMute(sessionId, muted)
|
||||||
|
session = updated
|
||||||
|
} else {
|
||||||
|
session = session?.copy(audioMuted = muted)
|
||||||
|
}
|
||||||
|
container.broadcastCoordinator.setAudioMuted(muted)
|
||||||
|
snackbar.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
when {
|
||||||
|
remote && muted -> R.string.broadcast_snackbar_muted_remote
|
||||||
|
remote && !muted -> R.string.broadcast_snackbar_unmuted_remote
|
||||||
|
muted -> R.string.broadcast_snackbar_muted
|
||||||
|
else -> R.string.broadcast_snackbar_unmuted
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackbar.showSnackbar(
|
||||||
|
context.getString(R.string.broadcast_snackbar_mute_error, e.message ?: genericError),
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
muteInFlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(sessionId, permissions.granted) {
|
LaunchedEffect(sessionId, permissions.granted) {
|
||||||
if (!permissions.granted) return@LaunchedEffect
|
if (!permissions.granted) return@LaunchedEffect
|
||||||
loading = true
|
loading = true
|
||||||
@@ -215,6 +252,9 @@ fun BroadcastScreen(
|
|||||||
container.sessionCable.onEndStream = {
|
container.sessionCable.onEndStream = {
|
||||||
scope.launch { applyRemoteStop() }
|
scope.launch { applyRemoteStop() }
|
||||||
}
|
}
|
||||||
|
container.sessionCable.onAudioMute = { muted ->
|
||||||
|
if (!muteInFlight) scope.launch { setAudioMuted(muted, remote = true) }
|
||||||
|
}
|
||||||
container.sessionCable.connect(sessionId, token, deviceRole = "camera")
|
container.sessionCable.connect(sessionId, token, deviceRole = "camera")
|
||||||
}
|
}
|
||||||
container.broadcastCoordinator.startService()
|
container.broadcastCoordinator.startService()
|
||||||
@@ -225,8 +265,10 @@ fun BroadcastScreen(
|
|||||||
val cfg = broadcastConfig(loaded)
|
val cfg = broadcastConfig(loaded)
|
||||||
thermalManager.updateBaseline(cfg)
|
thermalManager.updateBaseline(cfg)
|
||||||
container.broadcastCoordinator.engine.startBroadcast(cfg)
|
container.broadcastCoordinator.engine.startBroadcast(cfg)
|
||||||
|
container.broadcastCoordinator.setAudioMuted(loaded.audioMuted)
|
||||||
} else {
|
} else {
|
||||||
thermalManager.updateBaseline(broadcastConfig(loaded))
|
thermalManager.updateBaseline(broadcastConfig(loaded))
|
||||||
|
container.broadcastCoordinator.setAudioMuted(loaded.audioMuted)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
error = it.message ?: context.getString(R.string.broadcast_error_session_unavailable)
|
error = it.message ?: context.getString(R.string.broadcast_error_session_unavailable)
|
||||||
@@ -364,6 +406,7 @@ fun BroadcastScreen(
|
|||||||
container.sessionCable.onPauseStream = null
|
container.sessionCable.onPauseStream = null
|
||||||
container.sessionCable.onResumeStream = null
|
container.sessionCable.onResumeStream = null
|
||||||
container.sessionCable.onEndStream = null
|
container.sessionCable.onEndStream = null
|
||||||
|
container.sessionCable.onAudioMute = null
|
||||||
container.sessionCable.disconnect()
|
container.sessionCable.disconnect()
|
||||||
container.broadcastCoordinator.stopService()
|
container.broadcastCoordinator.stopService()
|
||||||
container.broadcastCoordinator.engine.stopBroadcast()
|
container.broadcastCoordinator.engine.stopBroadcast()
|
||||||
@@ -371,6 +414,7 @@ fun BroadcastScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isPaused = session?.isPaused == true || metrics.phase == BroadcastPhase.PAUSED
|
val isPaused = session?.isPaused == true || metrics.phase == BroadcastPhase.PAUSED
|
||||||
|
val audioMuted = session?.audioMuted == true || metrics.audioMuted
|
||||||
val statusText = when {
|
val statusText = when {
|
||||||
isPaused -> stringResource(R.string.broadcast_status_paused)
|
isPaused -> stringResource(R.string.broadcast_status_paused)
|
||||||
metrics.phase == BroadcastPhase.LIVE -> stringResource(R.string.broadcast_status_live)
|
metrics.phase == BroadcastPhase.LIVE -> stringResource(R.string.broadcast_status_live)
|
||||||
@@ -544,6 +588,10 @@ fun BroadcastScreen(
|
|||||||
if (isPaused) resumeStream() else pauseStream()
|
if (isPaused) resumeStream() else pauseStream()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onToggleAudioMute = {
|
||||||
|
scope.launch { setAudioMuted(!audioMuted, remote = false) }
|
||||||
|
},
|
||||||
|
audioMuted = audioMuted,
|
||||||
onTerminate = { scope.launch { stopStreamPermanently() } },
|
onTerminate = { scope.launch { stopStreamPermanently() } },
|
||||||
onShareLive = {
|
onShareLive = {
|
||||||
val url = shareUrl
|
val url = shareUrl
|
||||||
|
|||||||
+5
-13
@@ -57,7 +57,6 @@ import com.matchlivetv.match_live_tv.ui.components.LanguagePickerDialog
|
|||||||
import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark
|
import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark
|
||||||
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
||||||
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
|
||||||
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
|
|
||||||
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -237,18 +236,11 @@ fun MatchesScreen(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
MatchPrimaryButton(
|
||||||
MatchSecondaryButton(
|
label = stringResource(R.string.matches_new).uppercase(),
|
||||||
label = stringResource(R.string.matches_schedule).uppercase(),
|
onClick = { showNewMatchSheet = true },
|
||||||
onClick = { showScheduleSheet = true },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.weight(1f),
|
)
|
||||||
)
|
|
||||||
MatchPrimaryButton(
|
|
||||||
label = stringResource(R.string.matches_new).uppercase(),
|
|
||||||
onClick = { showNewMatchSheet = true },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
activeTeam?.let { team ->
|
activeTeam?.let { team ->
|
||||||
TeamPickerBar(
|
TeamPickerBar(
|
||||||
|
|||||||
@@ -21,13 +21,12 @@
|
|||||||
<string name="login_error_generic">Anmeldung fehlgeschlagen</string>
|
<string name="login_error_generic">Anmeldung fehlgeschlagen</string>
|
||||||
<string name="matches_title">Spiele</string>
|
<string name="matches_title">Spiele</string>
|
||||||
<string name="matches_hello">Hallo, %1$s</string>
|
<string name="matches_hello">Hallo, %1$s</string>
|
||||||
<string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.</string>
|
<string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein neues Spiel.</string>
|
||||||
<string name="matches_schedule">Geplantes Spiel</string>
|
|
||||||
<string name="matches_new">Neues Spiel</string>
|
<string name="matches_new">Neues Spiel</string>
|
||||||
<string name="matches_empty_title">Keine Spiele im Kalender</string>
|
<string name="matches_empty_title">Keine Spiele im Kalender</string>
|
||||||
<string name="matches_scheduled_title">Geplante Spiele</string>
|
<string name="matches_scheduled_title">Geplante Spiele</string>
|
||||||
<string name="matches_ready_title">Bereit zum Start</string>
|
<string name="matches_ready_title">Bereit zum Start</string>
|
||||||
<string name="matches_empty_hint">Plane ein Spiel oder starte ein neues mit «Neues Spiel».</string>
|
<string name="matches_empty_hint">Tippe auf «Neues Spiel», um im Voraus zu planen oder sofort zu starten.</string>
|
||||||
<string name="matches_active_team">Aktives Team: %1$s.</string>
|
<string name="matches_active_team">Aktives Team: %1$s.</string>
|
||||||
<string name="matches_multi_team_hint">Du hast mehrere Teams: prüfe die oben ausgewählte.</string>
|
<string name="matches_multi_team_hint">Du hast mehrere Teams: prüfe die oben ausgewählte.</string>
|
||||||
<string name="matches_no_other">Keine weiteren Spiele im Kalender.</string>
|
<string name="matches_no_other">Keine weiteren Spiele im Kalender.</string>
|
||||||
@@ -141,6 +140,11 @@
|
|||||||
<string name="broadcast_snackbar_resumed_remote">Von der Regie fortgesetzt</string>
|
<string name="broadcast_snackbar_resumed_remote">Von der Regie fortgesetzt</string>
|
||||||
<string name="broadcast_snackbar_resume_rtmp_error">RTMP-Fortsetzung: %1$s</string>
|
<string name="broadcast_snackbar_resume_rtmp_error">RTMP-Fortsetzung: %1$s</string>
|
||||||
<string name="broadcast_snackbar_closed_remote">Livestream von der Regie beendet</string>
|
<string name="broadcast_snackbar_closed_remote">Livestream von der Regie beendet</string>
|
||||||
|
<string name="broadcast_snackbar_muted">Audio stummgeschaltet</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted">Audio wieder aktiv</string>
|
||||||
|
<string name="broadcast_snackbar_muted_remote">Audio von der Regie stummgeschaltet</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted_remote">Audio von der Regie wieder aktiviert</string>
|
||||||
|
<string name="broadcast_snackbar_mute_error">Audio: %1$s</string>
|
||||||
<string name="broadcast_error_rtmp_missing">RTMP-URL fehlt</string>
|
<string name="broadcast_error_rtmp_missing">RTMP-URL fehlt</string>
|
||||||
<string name="broadcast_error_session_unavailable">Sitzung nicht verfügbar</string>
|
<string name="broadcast_error_session_unavailable">Sitzung nicht verfügbar</string>
|
||||||
<string name="broadcast_status_paused">PAUSE</string>
|
<string name="broadcast_status_paused">PAUSE</string>
|
||||||
@@ -164,6 +168,9 @@
|
|||||||
<string name="broadcast_next_period_cd">Nächste Periode</string>
|
<string name="broadcast_next_period_cd">Nächste Periode</string>
|
||||||
<string name="broadcast_resume_cd">Livestream fortsetzen</string>
|
<string name="broadcast_resume_cd">Livestream fortsetzen</string>
|
||||||
<string name="broadcast_pause_cd">Livestream pausieren</string>
|
<string name="broadcast_pause_cd">Livestream pausieren</string>
|
||||||
|
<string name="broadcast_mute_cd">Audio stummschalten</string>
|
||||||
|
<string name="broadcast_unmute_cd">Audio einschalten</string>
|
||||||
|
<string name="broadcast_audio_muted_label">Audio aus</string>
|
||||||
<string name="broadcast_terminate_cd">Livestream beenden</string>
|
<string name="broadcast_terminate_cd">Livestream beenden</string>
|
||||||
<string name="broadcast_team_home_label">HEIM</string>
|
<string name="broadcast_team_home_label">HEIM</string>
|
||||||
<string name="broadcast_team_away_label">GAST</string>
|
<string name="broadcast_team_away_label">GAST</string>
|
||||||
|
|||||||
@@ -21,13 +21,12 @@
|
|||||||
<string name="login_error_generic">Login failed</string>
|
<string name="login_error_generic">Login failed</string>
|
||||||
<string name="matches_title">Matches</string>
|
<string name="matches_title">Matches</string>
|
||||||
<string name="matches_hello">Hi, %1$s</string>
|
<string name="matches_hello">Hi, %1$s</string>
|
||||||
<string name="matches_subtitle">Resume a live stream or start a scheduled match.</string>
|
<string name="matches_subtitle">Resume a live stream or start a new match.</string>
|
||||||
<string name="matches_schedule">Scheduled match</string>
|
|
||||||
<string name="matches_new">New match</string>
|
<string name="matches_new">New match</string>
|
||||||
<string name="matches_empty_title">No matches on the calendar</string>
|
<string name="matches_empty_title">No matches on the calendar</string>
|
||||||
<string name="matches_scheduled_title">Scheduled matches</string>
|
<string name="matches_scheduled_title">Scheduled matches</string>
|
||||||
<string name="matches_ready_title">Ready to start</string>
|
<string name="matches_ready_title">Ready to start</string>
|
||||||
<string name="matches_empty_hint">Schedule a match or start a new one with «New match».</string>
|
<string name="matches_empty_hint">Tap «New match» to schedule ahead or start right away.</string>
|
||||||
<string name="matches_active_team">Active team: %1$s.</string>
|
<string name="matches_active_team">Active team: %1$s.</string>
|
||||||
<string name="matches_multi_team_hint">You have multiple teams: check the one selected above.</string>
|
<string name="matches_multi_team_hint">You have multiple teams: check the one selected above.</string>
|
||||||
<string name="matches_no_other">No other matches on the calendar.</string>
|
<string name="matches_no_other">No other matches on the calendar.</string>
|
||||||
@@ -141,6 +140,11 @@
|
|||||||
<string name="broadcast_snackbar_resumed_remote">Resumed from control room</string>
|
<string name="broadcast_snackbar_resumed_remote">Resumed from control room</string>
|
||||||
<string name="broadcast_snackbar_resume_rtmp_error">RTMP resume: %1$s</string>
|
<string name="broadcast_snackbar_resume_rtmp_error">RTMP resume: %1$s</string>
|
||||||
<string name="broadcast_snackbar_closed_remote">Live closed from control room</string>
|
<string name="broadcast_snackbar_closed_remote">Live closed from control room</string>
|
||||||
|
<string name="broadcast_snackbar_muted">Audio muted</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted">Audio restored</string>
|
||||||
|
<string name="broadcast_snackbar_muted_remote">Audio muted from control room</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted_remote">Audio restored from control room</string>
|
||||||
|
<string name="broadcast_snackbar_mute_error">Audio: %1$s</string>
|
||||||
<string name="broadcast_error_rtmp_missing">Missing RTMP URL</string>
|
<string name="broadcast_error_rtmp_missing">Missing RTMP URL</string>
|
||||||
<string name="broadcast_error_session_unavailable">Session not available</string>
|
<string name="broadcast_error_session_unavailable">Session not available</string>
|
||||||
<string name="broadcast_status_paused">PAUSED</string>
|
<string name="broadcast_status_paused">PAUSED</string>
|
||||||
@@ -164,6 +168,9 @@
|
|||||||
<string name="broadcast_next_period_cd">Next period</string>
|
<string name="broadcast_next_period_cd">Next period</string>
|
||||||
<string name="broadcast_resume_cd">Resume live</string>
|
<string name="broadcast_resume_cd">Resume live</string>
|
||||||
<string name="broadcast_pause_cd">Pause live</string>
|
<string name="broadcast_pause_cd">Pause live</string>
|
||||||
|
<string name="broadcast_mute_cd">Mute audio</string>
|
||||||
|
<string name="broadcast_unmute_cd">Unmute audio</string>
|
||||||
|
<string name="broadcast_audio_muted_label">Audio off</string>
|
||||||
<string name="broadcast_terminate_cd">End live</string>
|
<string name="broadcast_terminate_cd">End live</string>
|
||||||
<string name="broadcast_team_home_label">HOME</string>
|
<string name="broadcast_team_home_label">HOME</string>
|
||||||
<string name="broadcast_team_away_label">AWAY</string>
|
<string name="broadcast_team_away_label">AWAY</string>
|
||||||
|
|||||||
@@ -21,13 +21,12 @@
|
|||||||
<string name="login_error_generic">Error de acceso</string>
|
<string name="login_error_generic">Error de acceso</string>
|
||||||
<string name="matches_title">Partidos</string>
|
<string name="matches_title">Partidos</string>
|
||||||
<string name="matches_hello">Hola, %1$s</string>
|
<string name="matches_hello">Hola, %1$s</string>
|
||||||
<string name="matches_subtitle">Reanuda un directo en curso o inicia un partido programado.</string>
|
<string name="matches_subtitle">Reanuda un directo en curso o inicia un nuevo partido.</string>
|
||||||
<string name="matches_schedule">Partido programado</string>
|
|
||||||
<string name="matches_new">Nuevo partido</string>
|
<string name="matches_new">Nuevo partido</string>
|
||||||
<string name="matches_empty_title">Ningún partido en el calendario</string>
|
<string name="matches_empty_title">Ningún partido en el calendario</string>
|
||||||
<string name="matches_scheduled_title">Partidos programados</string>
|
<string name="matches_scheduled_title">Partidos programados</string>
|
||||||
<string name="matches_ready_title">Listos para empezar</string>
|
<string name="matches_ready_title">Listos para empezar</string>
|
||||||
<string name="matches_empty_hint">Programa un partido o inicia uno nuevo con «Nuevo partido».</string>
|
<string name="matches_empty_hint">Toca «Nuevo partido» para programar con antelación o empezar ya.</string>
|
||||||
<string name="matches_active_team">Equipo activo: %1$s.</string>
|
<string name="matches_active_team">Equipo activo: %1$s.</string>
|
||||||
<string name="matches_multi_team_hint">Tienes varios equipos: comprueba el seleccionado arriba.</string>
|
<string name="matches_multi_team_hint">Tienes varios equipos: comprueba el seleccionado arriba.</string>
|
||||||
<string name="matches_no_other">Ningún otro partido en el calendario.</string>
|
<string name="matches_no_other">Ningún otro partido en el calendario.</string>
|
||||||
@@ -141,6 +140,11 @@
|
|||||||
<string name="broadcast_snackbar_resumed_remote">Reanudado desde regie</string>
|
<string name="broadcast_snackbar_resumed_remote">Reanudado desde regie</string>
|
||||||
<string name="broadcast_snackbar_resume_rtmp_error">Reanudación RTMP: %1$s</string>
|
<string name="broadcast_snackbar_resume_rtmp_error">Reanudación RTMP: %1$s</string>
|
||||||
<string name="broadcast_snackbar_closed_remote">Directo cerrado desde regie</string>
|
<string name="broadcast_snackbar_closed_remote">Directo cerrado desde regie</string>
|
||||||
|
<string name="broadcast_snackbar_muted">Audio silenciado</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted">Audio reactivado</string>
|
||||||
|
<string name="broadcast_snackbar_muted_remote">Audio silenciado desde regie</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted_remote">Audio reactivado desde regie</string>
|
||||||
|
<string name="broadcast_snackbar_mute_error">Audio: %1$s</string>
|
||||||
<string name="broadcast_error_rtmp_missing">Falta la URL RTMP</string>
|
<string name="broadcast_error_rtmp_missing">Falta la URL RTMP</string>
|
||||||
<string name="broadcast_error_session_unavailable">Sesión no disponible</string>
|
<string name="broadcast_error_session_unavailable">Sesión no disponible</string>
|
||||||
<string name="broadcast_status_paused">PAUSA</string>
|
<string name="broadcast_status_paused">PAUSA</string>
|
||||||
@@ -164,6 +168,9 @@
|
|||||||
<string name="broadcast_next_period_cd">Periodo siguiente</string>
|
<string name="broadcast_next_period_cd">Periodo siguiente</string>
|
||||||
<string name="broadcast_resume_cd">Reanudar directo</string>
|
<string name="broadcast_resume_cd">Reanudar directo</string>
|
||||||
<string name="broadcast_pause_cd">Pausar directo</string>
|
<string name="broadcast_pause_cd">Pausar directo</string>
|
||||||
|
<string name="broadcast_mute_cd">Silenciar audio</string>
|
||||||
|
<string name="broadcast_unmute_cd">Reactivar audio</string>
|
||||||
|
<string name="broadcast_audio_muted_label">Audio off</string>
|
||||||
<string name="broadcast_terminate_cd">Terminar directo</string>
|
<string name="broadcast_terminate_cd">Terminar directo</string>
|
||||||
<string name="broadcast_team_home_label">LOCAL</string>
|
<string name="broadcast_team_home_label">LOCAL</string>
|
||||||
<string name="broadcast_team_away_label">VISITANTE</string>
|
<string name="broadcast_team_away_label">VISITANTE</string>
|
||||||
|
|||||||
@@ -21,13 +21,12 @@
|
|||||||
<string name="login_error_generic">Échec de la connexion</string>
|
<string name="login_error_generic">Échec de la connexion</string>
|
||||||
<string name="matches_title">Matchs</string>
|
<string name="matches_title">Matchs</string>
|
||||||
<string name="matches_hello">Bonjour, %1$s</string>
|
<string name="matches_hello">Bonjour, %1$s</string>
|
||||||
<string name="matches_subtitle">Reprenez un direct en cours ou démarrez un match programmé.</string>
|
<string name="matches_subtitle">Reprenez un direct en cours ou démarrez un nouveau match.</string>
|
||||||
<string name="matches_schedule">Match programmé</string>
|
|
||||||
<string name="matches_new">Nouveau match</string>
|
<string name="matches_new">Nouveau match</string>
|
||||||
<string name="matches_empty_title">Aucun match au calendrier</string>
|
<string name="matches_empty_title">Aucun match au calendrier</string>
|
||||||
<string name="matches_scheduled_title">Matchs programmés</string>
|
<string name="matches_scheduled_title">Matchs programmés</string>
|
||||||
<string name="matches_ready_title">Prêts à démarrer</string>
|
<string name="matches_ready_title">Prêts à démarrer</string>
|
||||||
<string name="matches_empty_hint">Programmez un match ou démarrez-en un avec « Nouveau match ».</string>
|
<string name="matches_empty_hint">Touchez « Nouveau match » pour planifier à l\'avance ou démarrer tout de suite.</string>
|
||||||
<string name="matches_active_team">Équipe active : %1$s.</string>
|
<string name="matches_active_team">Équipe active : %1$s.</string>
|
||||||
<string name="matches_multi_team_hint">Vous avez plusieurs équipes : vérifiez celle sélectionnée ci-dessus.</string>
|
<string name="matches_multi_team_hint">Vous avez plusieurs équipes : vérifiez celle sélectionnée ci-dessus.</string>
|
||||||
<string name="matches_no_other">Aucun autre match au calendrier.</string>
|
<string name="matches_no_other">Aucun autre match au calendrier.</string>
|
||||||
@@ -141,6 +140,11 @@
|
|||||||
<string name="broadcast_snackbar_resumed_remote">Repris depuis la régie</string>
|
<string name="broadcast_snackbar_resumed_remote">Repris depuis la régie</string>
|
||||||
<string name="broadcast_snackbar_resume_rtmp_error">Reprise RTMP : %1$s</string>
|
<string name="broadcast_snackbar_resume_rtmp_error">Reprise RTMP : %1$s</string>
|
||||||
<string name="broadcast_snackbar_closed_remote">Direct fermé depuis la régie</string>
|
<string name="broadcast_snackbar_closed_remote">Direct fermé depuis la régie</string>
|
||||||
|
<string name="broadcast_snackbar_muted">Audio coupé</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted">Audio rétabli</string>
|
||||||
|
<string name="broadcast_snackbar_muted_remote">Audio coupé depuis la régie</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted_remote">Audio rétabli depuis la régie</string>
|
||||||
|
<string name="broadcast_snackbar_mute_error">Audio : %1$s</string>
|
||||||
<string name="broadcast_error_rtmp_missing">URL RTMP manquante</string>
|
<string name="broadcast_error_rtmp_missing">URL RTMP manquante</string>
|
||||||
<string name="broadcast_error_session_unavailable">Session non disponible</string>
|
<string name="broadcast_error_session_unavailable">Session non disponible</string>
|
||||||
<string name="broadcast_status_paused">PAUSE</string>
|
<string name="broadcast_status_paused">PAUSE</string>
|
||||||
@@ -164,6 +168,9 @@
|
|||||||
<string name="broadcast_next_period_cd">Période suivante</string>
|
<string name="broadcast_next_period_cd">Période suivante</string>
|
||||||
<string name="broadcast_resume_cd">Reprendre le direct</string>
|
<string name="broadcast_resume_cd">Reprendre le direct</string>
|
||||||
<string name="broadcast_pause_cd">Mettre le direct en pause</string>
|
<string name="broadcast_pause_cd">Mettre le direct en pause</string>
|
||||||
|
<string name="broadcast_mute_cd">Couper l\'audio</string>
|
||||||
|
<string name="broadcast_unmute_cd">Réactiver l\'audio</string>
|
||||||
|
<string name="broadcast_audio_muted_label">Audio off</string>
|
||||||
<string name="broadcast_terminate_cd">Terminer le direct</string>
|
<string name="broadcast_terminate_cd">Terminer le direct</string>
|
||||||
<string name="broadcast_team_home_label">DOMICILE</string>
|
<string name="broadcast_team_home_label">DOMICILE</string>
|
||||||
<string name="broadcast_team_away_label">EXTÉRIEUR</string>
|
<string name="broadcast_team_away_label">EXTÉRIEUR</string>
|
||||||
|
|||||||
@@ -21,13 +21,12 @@
|
|||||||
<string name="login_error_generic">Login fallito</string>
|
<string name="login_error_generic">Login fallito</string>
|
||||||
<string name="matches_title">Partite</string>
|
<string name="matches_title">Partite</string>
|
||||||
<string name="matches_hello">Ciao, %1$s</string>
|
<string name="matches_hello">Ciao, %1$s</string>
|
||||||
<string name="matches_subtitle">Riprendi una diretta in corso o avvia una partita programmata.</string>
|
<string name="matches_subtitle">Riprendi una diretta in corso o avvia una nuova partita.</string>
|
||||||
<string name="matches_schedule">Partita programmata</string>
|
|
||||||
<string name="matches_new">Nuova partita</string>
|
<string name="matches_new">Nuova partita</string>
|
||||||
<string name="matches_empty_title">Nessuna partita in calendario</string>
|
<string name="matches_empty_title">Nessuna partita in calendario</string>
|
||||||
<string name="matches_scheduled_title">Partite programmate</string>
|
<string name="matches_scheduled_title">Partite programmate</string>
|
||||||
<string name="matches_ready_title">Pronte da avviare</string>
|
<string name="matches_ready_title">Pronte da avviare</string>
|
||||||
<string name="matches_empty_hint">Programma una partita o avviane una nuova con «Nuova partita».</string>
|
<string name="matches_empty_hint">Tocca «Nuova partita» per programmare in anticipo o avviare subito.</string>
|
||||||
<string name="matches_active_team">Squadra attiva: %1$s.</string>
|
<string name="matches_active_team">Squadra attiva: %1$s.</string>
|
||||||
<string name="matches_multi_team_hint">Hai più squadre: verifica quella selezionata sopra.</string>
|
<string name="matches_multi_team_hint">Hai più squadre: verifica quella selezionata sopra.</string>
|
||||||
<string name="matches_no_other">Nessuna altra partita in calendario.</string>
|
<string name="matches_no_other">Nessuna altra partita in calendario.</string>
|
||||||
@@ -141,6 +140,11 @@
|
|||||||
<string name="broadcast_snackbar_resumed_remote">Ripresa dalla regia</string>
|
<string name="broadcast_snackbar_resumed_remote">Ripresa dalla regia</string>
|
||||||
<string name="broadcast_snackbar_resume_rtmp_error">Ripresa RTMP: %1$s</string>
|
<string name="broadcast_snackbar_resume_rtmp_error">Ripresa RTMP: %1$s</string>
|
||||||
<string name="broadcast_snackbar_closed_remote">Diretta chiusa dalla regia</string>
|
<string name="broadcast_snackbar_closed_remote">Diretta chiusa dalla regia</string>
|
||||||
|
<string name="broadcast_snackbar_muted">Audio silenziato</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted">Audio riattivato</string>
|
||||||
|
<string name="broadcast_snackbar_muted_remote">Audio silenziato dalla regia</string>
|
||||||
|
<string name="broadcast_snackbar_unmuted_remote">Audio riattivato dalla regia</string>
|
||||||
|
<string name="broadcast_snackbar_mute_error">Audio: %1$s</string>
|
||||||
<string name="broadcast_error_rtmp_missing">URL RTMP mancante</string>
|
<string name="broadcast_error_rtmp_missing">URL RTMP mancante</string>
|
||||||
<string name="broadcast_error_session_unavailable">Sessione non disponibile</string>
|
<string name="broadcast_error_session_unavailable">Sessione non disponibile</string>
|
||||||
<string name="broadcast_status_paused">PAUSA</string>
|
<string name="broadcast_status_paused">PAUSA</string>
|
||||||
@@ -164,6 +168,9 @@
|
|||||||
<string name="broadcast_next_period_cd">Periodo successivo</string>
|
<string name="broadcast_next_period_cd">Periodo successivo</string>
|
||||||
<string name="broadcast_resume_cd">Riprendi diretta</string>
|
<string name="broadcast_resume_cd">Riprendi diretta</string>
|
||||||
<string name="broadcast_pause_cd">Pausa diretta</string>
|
<string name="broadcast_pause_cd">Pausa diretta</string>
|
||||||
|
<string name="broadcast_mute_cd">Silenzia audio</string>
|
||||||
|
<string name="broadcast_unmute_cd">Riattiva audio</string>
|
||||||
|
<string name="broadcast_audio_muted_label">Audio off</string>
|
||||||
<string name="broadcast_terminate_cd">Termina diretta</string>
|
<string name="broadcast_terminate_cd">Termina diretta</string>
|
||||||
<string name="broadcast_team_home_label">CASA</string>
|
<string name="broadcast_team_home_label">CASA</string>
|
||||||
<string name="broadcast_team_away_label">OSPITE</string>
|
<string name="broadcast_team_away_label">OSPITE</string>
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package com.matchlivetv.match_live_tv.data.cable
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AudioMuteCommandsTest {
|
||||||
|
@Test
|
||||||
|
fun commandMuteAudio() {
|
||||||
|
assertEquals(true, AudioMuteCommands.mutedFromPayload(type = "command", action = "mute_audio"))
|
||||||
|
assertEquals(false, AudioMuteCommands.mutedFromPayload(type = "command", action = "unmute_audio"))
|
||||||
|
assertNull(AudioMuteCommands.mutedFromPayload(type = "command", action = "pause_stream"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun streamEventAudioMuted() {
|
||||||
|
assertEquals(true, AudioMuteCommands.mutedFromPayload(type = "stream_event", event = "audio_muted", muted = true))
|
||||||
|
assertEquals(false, AudioMuteCommands.mutedFromPayload(type = "stream_event", event = "audio_muted", muted = false))
|
||||||
|
assertNull(AudioMuteCommands.mutedFromPayload(type = "stream_event", event = "paused"))
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +1,102 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme LastUpgradeVersion="1600" version="1.7">
|
<Scheme
|
||||||
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
|
LastUpgradeVersion = "2660"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
|
<BuildActionEntry
|
||||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
|
||||||
|
BuildableName = "MatchLiveTv.app"
|
||||||
|
BlueprintName = "MatchLiveTv"
|
||||||
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
|
<BuildActionEntry
|
||||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F6079500E62048DB89AE3866" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "NO"
|
||||||
|
buildForProfiling = "NO"
|
||||||
|
buildForArchiving = "NO"
|
||||||
|
buildForAnalyzing = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "20C695DBFB4042879308B0F5"
|
||||||
|
BuildableName = "MatchLiveTvTests.xctest"
|
||||||
|
BlueprintName = "MatchLiveTvTests"
|
||||||
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference skipped="NO">
|
<TestableReference
|
||||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="F6079500E62048DB89AE3866" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "20C695DBFB4042879308B0F5"
|
||||||
|
BuildableName = "MatchLiveTvTests.xctest"
|
||||||
|
BlueprintName = "MatchLiveTvTests"
|
||||||
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
|
<LaunchAction
|
||||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
buildConfiguration = "Debug"
|
||||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
|
||||||
|
BuildableName = "MatchLiveTv.app"
|
||||||
|
BlueprintName = "MatchLiveTv"
|
||||||
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
|
<ProfileAction
|
||||||
<BuildableProductRunnable runnableDebuggingMode="0">
|
buildConfiguration = "Release"
|
||||||
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="768EC47B451F4731A05BE1F9" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "F54F6C97361C4E8A96AEDD11"
|
||||||
|
BuildableName = "MatchLiveTv.app"
|
||||||
|
BlueprintName = "MatchLiveTv"
|
||||||
|
ReferencedContainer = "container:MatchLiveTv.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
<AnalyzeAction buildConfiguration="Debug"/>
|
<AnalyzeAction
|
||||||
<ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
</Scheme>
|
</Scheme>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ enum L10n {
|
|||||||
"broadcast.error.rtmp.url.invalid": "URL RTMP non valido",
|
"broadcast.error.rtmp.url.invalid": "URL RTMP non valido",
|
||||||
"broadcast.error.session.unavailable": "Sessione non disponibile",
|
"broadcast.error.session.unavailable": "Sessione non disponibile",
|
||||||
"broadcast.hide.controls.cd": "Nascondi controlli",
|
"broadcast.hide.controls.cd": "Nascondi controlli",
|
||||||
|
"broadcast.mute.cd": "Silenzia audio",
|
||||||
"broadcast.next.period.cd": "Periodo successivo",
|
"broadcast.next.period.cd": "Periodo successivo",
|
||||||
"broadcast.pause.cd": "Pausa diretta",
|
"broadcast.pause.cd": "Pausa diretta",
|
||||||
"broadcast.period.label.timed": "%1$d° tempo",
|
"broadcast.period.label.timed": "%1$d° tempo",
|
||||||
@@ -140,6 +141,9 @@ enum L10n {
|
|||||||
"broadcast.side.away": "ospite",
|
"broadcast.side.away": "ospite",
|
||||||
"broadcast.side.home": "casa",
|
"broadcast.side.home": "casa",
|
||||||
"broadcast.snackbar.closed.remote": "Diretta chiusa dalla regia",
|
"broadcast.snackbar.closed.remote": "Diretta chiusa dalla regia",
|
||||||
|
"broadcast.snackbar.mute.error": "Audio: %1$@",
|
||||||
|
"broadcast.snackbar.muted": "Audio silenziato",
|
||||||
|
"broadcast.snackbar.muted.remote": "Audio silenziato dalla regia",
|
||||||
"broadcast.snackbar.pause.error": "Pausa: %1$@",
|
"broadcast.snackbar.pause.error": "Pausa: %1$@",
|
||||||
"broadcast.snackbar.paused": "Diretta in pausa",
|
"broadcast.snackbar.paused": "Diretta in pausa",
|
||||||
"broadcast.snackbar.paused.remote": "Pausa dalla regia",
|
"broadcast.snackbar.paused.remote": "Pausa dalla regia",
|
||||||
@@ -148,6 +152,8 @@ enum L10n {
|
|||||||
"broadcast.snackbar.resume.rtmp.error": "Ripresa RTMP: %1$@",
|
"broadcast.snackbar.resume.rtmp.error": "Ripresa RTMP: %1$@",
|
||||||
"broadcast.snackbar.resumed": "Diretta ripresa",
|
"broadcast.snackbar.resumed": "Diretta ripresa",
|
||||||
"broadcast.snackbar.resumed.remote": "Ripresa dalla regia",
|
"broadcast.snackbar.resumed.remote": "Ripresa dalla regia",
|
||||||
|
"broadcast.snackbar.unmuted": "Audio riattivato",
|
||||||
|
"broadcast.snackbar.unmuted.remote": "Audio riattivato dalla regia",
|
||||||
"broadcast.status.connecting": "CONNESSIONE…",
|
"broadcast.status.connecting": "CONNESSIONE…",
|
||||||
"broadcast.status.error.fallback": "ERRORE",
|
"broadcast.status.error.fallback": "ERRORE",
|
||||||
"broadcast.status.live": "IN DIRETTA",
|
"broadcast.status.live": "IN DIRETTA",
|
||||||
@@ -164,6 +170,8 @@ enum L10n {
|
|||||||
"broadcast.tooltip.add.point": "Aggiungi punto %1$@",
|
"broadcast.tooltip.add.point": "Aggiungi punto %1$@",
|
||||||
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
||||||
"broadcast.tooltip.remove.point": "Togli punto %1$@",
|
"broadcast.tooltip.remove.point": "Togli punto %1$@",
|
||||||
|
"broadcast.unmute.cd": "Riattiva audio",
|
||||||
|
"broadcast.audio.muted.label": "Audio off",
|
||||||
"common.close": "Chiudi",
|
"common.close": "Chiudi",
|
||||||
"common.error.generic": "errore",
|
"common.error.generic": "errore",
|
||||||
"common.error.title": "Errore",
|
"common.error.title": "Errore",
|
||||||
@@ -194,7 +202,7 @@ enum L10n {
|
|||||||
"match.status.start": "AVVIA",
|
"match.status.start": "AVVIA",
|
||||||
"matches.active.team": "Squadra attiva: %1$@.",
|
"matches.active.team": "Squadra attiva: %1$@.",
|
||||||
"matches.change": "Cambia",
|
"matches.change": "Cambia",
|
||||||
"matches.empty.hint": "Programma una partita o avviane una nuova con «Nuova partita».",
|
"matches.empty.hint": "Tocca «Nuova partita» per programmare in anticipo o avviare subito.",
|
||||||
"matches.empty.title": "Nessuna partita in calendario",
|
"matches.empty.title": "Nessuna partita in calendario",
|
||||||
"matches.hello": "Ciao, %1$@",
|
"matches.hello": "Ciao, %1$@",
|
||||||
"matches.load.error": "Errore caricamento",
|
"matches.load.error": "Errore caricamento",
|
||||||
@@ -212,9 +220,8 @@ enum L10n {
|
|||||||
"matches.no.team.title": "Nessuna squadra assegnata",
|
"matches.no.team.title": "Nessuna squadra assegnata",
|
||||||
"matches.ready.title": "Pronte da avviare",
|
"matches.ready.title": "Pronte da avviare",
|
||||||
"matches.retry": "Riprova",
|
"matches.retry": "Riprova",
|
||||||
"matches.schedule": "Partita programmata",
|
|
||||||
"matches.scheduled.title": "Partite programmate",
|
"matches.scheduled.title": "Partite programmate",
|
||||||
"matches.subtitle": "Riprendi una diretta in corso o avvia una partita programmata.",
|
"matches.subtitle": "Riprendi una diretta in corso o avvia una nuova partita.",
|
||||||
"matches.tap.change.team": "Tocca per cambiare squadra",
|
"matches.tap.change.team": "Tocca per cambiare squadra",
|
||||||
"matches.team.for.live": "Squadra per la diretta",
|
"matches.team.for.live": "Squadra per la diretta",
|
||||||
"matches.title": "Partite",
|
"matches.title": "Partite",
|
||||||
@@ -381,7 +388,7 @@ enum L10n {
|
|||||||
"account.save.password": "Update password",
|
"account.save.password": "Update password",
|
||||||
"account.save.profile": "Save profile",
|
"account.save.profile": "Save profile",
|
||||||
"account.title": "Account",
|
"account.title": "Account",
|
||||||
"forgot.password.lead": "Enter your account email: we'll send you a link to reset your password.",
|
"forgot.password.lead": "Enter your account email: we will send you a link to reset your password.",
|
||||||
"forgot.password.submit": "Send reset link",
|
"forgot.password.submit": "Send reset link",
|
||||||
"forgot.password.success": "If the email is registered, you will receive a password reset link shortly.",
|
"forgot.password.success": "If the email is registered, you will receive a password reset link shortly.",
|
||||||
"forgot.password.title": "Forgot password",
|
"forgot.password.title": "Forgot password",
|
||||||
@@ -410,6 +417,7 @@ enum L10n {
|
|||||||
"broadcast.error.rtmp.url.invalid": "Invalid RTMP URL",
|
"broadcast.error.rtmp.url.invalid": "Invalid RTMP URL",
|
||||||
"broadcast.error.session.unavailable": "Session not available",
|
"broadcast.error.session.unavailable": "Session not available",
|
||||||
"broadcast.hide.controls.cd": "Hide controls",
|
"broadcast.hide.controls.cd": "Hide controls",
|
||||||
|
"broadcast.mute.cd": "Mute audio",
|
||||||
"broadcast.next.period.cd": "Next period",
|
"broadcast.next.period.cd": "Next period",
|
||||||
"broadcast.pause.cd": "Pause live",
|
"broadcast.pause.cd": "Pause live",
|
||||||
"broadcast.period.label.timed": "Period %1$d",
|
"broadcast.period.label.timed": "Period %1$d",
|
||||||
@@ -431,6 +439,9 @@ enum L10n {
|
|||||||
"broadcast.side.away": "away",
|
"broadcast.side.away": "away",
|
||||||
"broadcast.side.home": "home",
|
"broadcast.side.home": "home",
|
||||||
"broadcast.snackbar.closed.remote": "Live closed from control room",
|
"broadcast.snackbar.closed.remote": "Live closed from control room",
|
||||||
|
"broadcast.snackbar.mute.error": "Audio: %1$@",
|
||||||
|
"broadcast.snackbar.muted": "Audio muted",
|
||||||
|
"broadcast.snackbar.muted.remote": "Audio muted from control room",
|
||||||
"broadcast.snackbar.pause.error": "Pause: %1$@",
|
"broadcast.snackbar.pause.error": "Pause: %1$@",
|
||||||
"broadcast.snackbar.paused": "Live paused",
|
"broadcast.snackbar.paused": "Live paused",
|
||||||
"broadcast.snackbar.paused.remote": "Paused from control room",
|
"broadcast.snackbar.paused.remote": "Paused from control room",
|
||||||
@@ -439,6 +450,8 @@ enum L10n {
|
|||||||
"broadcast.snackbar.resume.rtmp.error": "RTMP resume: %1$@",
|
"broadcast.snackbar.resume.rtmp.error": "RTMP resume: %1$@",
|
||||||
"broadcast.snackbar.resumed": "Live resumed",
|
"broadcast.snackbar.resumed": "Live resumed",
|
||||||
"broadcast.snackbar.resumed.remote": "Resumed from control room",
|
"broadcast.snackbar.resumed.remote": "Resumed from control room",
|
||||||
|
"broadcast.snackbar.unmuted": "Audio restored",
|
||||||
|
"broadcast.snackbar.unmuted.remote": "Audio restored from control room",
|
||||||
"broadcast.status.connecting": "CONNECTING…",
|
"broadcast.status.connecting": "CONNECTING…",
|
||||||
"broadcast.status.error.fallback": "ERROR",
|
"broadcast.status.error.fallback": "ERROR",
|
||||||
"broadcast.status.live": "LIVE",
|
"broadcast.status.live": "LIVE",
|
||||||
@@ -455,6 +468,8 @@ enum L10n {
|
|||||||
"broadcast.tooltip.add.point": "Add point %1$@",
|
"broadcast.tooltip.add.point": "Add point %1$@",
|
||||||
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
||||||
"broadcast.tooltip.remove.point": "Remove point %1$@",
|
"broadcast.tooltip.remove.point": "Remove point %1$@",
|
||||||
|
"broadcast.unmute.cd": "Unmute audio",
|
||||||
|
"broadcast.audio.muted.label": "Audio off",
|
||||||
"common.close": "Close",
|
"common.close": "Close",
|
||||||
"common.error.generic": "error",
|
"common.error.generic": "error",
|
||||||
"common.error.title": "Error",
|
"common.error.title": "Error",
|
||||||
@@ -485,7 +500,7 @@ enum L10n {
|
|||||||
"match.status.start": "START",
|
"match.status.start": "START",
|
||||||
"matches.active.team": "Active team: %1$@.",
|
"matches.active.team": "Active team: %1$@.",
|
||||||
"matches.change": "Change",
|
"matches.change": "Change",
|
||||||
"matches.empty.hint": "Schedule a match or start a new one with «New match».",
|
"matches.empty.hint": "Tap «New match» to schedule ahead or start right away.",
|
||||||
"matches.empty.title": "No matches on the calendar",
|
"matches.empty.title": "No matches on the calendar",
|
||||||
"matches.hello": "Hi, %1$@",
|
"matches.hello": "Hi, %1$@",
|
||||||
"matches.load.error": "Loading error",
|
"matches.load.error": "Loading error",
|
||||||
@@ -503,9 +518,8 @@ enum L10n {
|
|||||||
"matches.no.team.title": "No team assigned",
|
"matches.no.team.title": "No team assigned",
|
||||||
"matches.ready.title": "Ready to start",
|
"matches.ready.title": "Ready to start",
|
||||||
"matches.retry": "Retry",
|
"matches.retry": "Retry",
|
||||||
"matches.schedule": "Scheduled match",
|
|
||||||
"matches.scheduled.title": "Scheduled matches",
|
"matches.scheduled.title": "Scheduled matches",
|
||||||
"matches.subtitle": "Resume a live stream or start a scheduled match.",
|
"matches.subtitle": "Resume a live stream or start a new match.",
|
||||||
"matches.tap.change.team": "Tap to change team",
|
"matches.tap.change.team": "Tap to change team",
|
||||||
"matches.team.for.live": "Team for the live stream",
|
"matches.team.for.live": "Team for the live stream",
|
||||||
"matches.title": "Matches",
|
"matches.title": "Matches",
|
||||||
@@ -701,6 +715,7 @@ enum L10n {
|
|||||||
"broadcast.error.rtmp.url.invalid": "URL RTMP non valide",
|
"broadcast.error.rtmp.url.invalid": "URL RTMP non valide",
|
||||||
"broadcast.error.session.unavailable": "Session non disponible",
|
"broadcast.error.session.unavailable": "Session non disponible",
|
||||||
"broadcast.hide.controls.cd": "Masquer les commandes",
|
"broadcast.hide.controls.cd": "Masquer les commandes",
|
||||||
|
"broadcast.mute.cd": "Couper l'audio",
|
||||||
"broadcast.next.period.cd": "Période suivante",
|
"broadcast.next.period.cd": "Période suivante",
|
||||||
"broadcast.pause.cd": "Mettre le direct en pause",
|
"broadcast.pause.cd": "Mettre le direct en pause",
|
||||||
"broadcast.period.label.timed": "Période %1$d",
|
"broadcast.period.label.timed": "Période %1$d",
|
||||||
@@ -722,6 +737,9 @@ enum L10n {
|
|||||||
"broadcast.side.away": "extérieur",
|
"broadcast.side.away": "extérieur",
|
||||||
"broadcast.side.home": "domicile",
|
"broadcast.side.home": "domicile",
|
||||||
"broadcast.snackbar.closed.remote": "Direct fermé depuis la régie",
|
"broadcast.snackbar.closed.remote": "Direct fermé depuis la régie",
|
||||||
|
"broadcast.snackbar.mute.error": "Audio : %1$@",
|
||||||
|
"broadcast.snackbar.muted": "Audio coupé",
|
||||||
|
"broadcast.snackbar.muted.remote": "Audio coupé depuis la régie",
|
||||||
"broadcast.snackbar.pause.error": "Pause : %1$@",
|
"broadcast.snackbar.pause.error": "Pause : %1$@",
|
||||||
"broadcast.snackbar.paused": "Direct en pause",
|
"broadcast.snackbar.paused": "Direct en pause",
|
||||||
"broadcast.snackbar.paused.remote": "Mis en pause depuis la régie",
|
"broadcast.snackbar.paused.remote": "Mis en pause depuis la régie",
|
||||||
@@ -730,6 +748,8 @@ enum L10n {
|
|||||||
"broadcast.snackbar.resume.rtmp.error": "Reprise RTMP : %1$@",
|
"broadcast.snackbar.resume.rtmp.error": "Reprise RTMP : %1$@",
|
||||||
"broadcast.snackbar.resumed": "Direct repris",
|
"broadcast.snackbar.resumed": "Direct repris",
|
||||||
"broadcast.snackbar.resumed.remote": "Repris depuis la régie",
|
"broadcast.snackbar.resumed.remote": "Repris depuis la régie",
|
||||||
|
"broadcast.snackbar.unmuted": "Audio rétabli",
|
||||||
|
"broadcast.snackbar.unmuted.remote": "Audio rétabli depuis la régie",
|
||||||
"broadcast.status.connecting": "CONNEXION…",
|
"broadcast.status.connecting": "CONNEXION…",
|
||||||
"broadcast.status.error.fallback": "ERREUR",
|
"broadcast.status.error.fallback": "ERREUR",
|
||||||
"broadcast.status.live": "EN DIRECT",
|
"broadcast.status.live": "EN DIRECT",
|
||||||
@@ -746,6 +766,8 @@ enum L10n {
|
|||||||
"broadcast.tooltip.add.point": "Ajouter un point %1$@",
|
"broadcast.tooltip.add.point": "Ajouter un point %1$@",
|
||||||
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
||||||
"broadcast.tooltip.remove.point": "Retirer un point %1$@",
|
"broadcast.tooltip.remove.point": "Retirer un point %1$@",
|
||||||
|
"broadcast.unmute.cd": "Réactiver l'audio",
|
||||||
|
"broadcast.audio.muted.label": "Audio off",
|
||||||
"common.close": "Fermer",
|
"common.close": "Fermer",
|
||||||
"common.error.generic": "erreur",
|
"common.error.generic": "erreur",
|
||||||
"common.error.title": "Erreur",
|
"common.error.title": "Erreur",
|
||||||
@@ -776,7 +798,7 @@ enum L10n {
|
|||||||
"match.status.start": "DÉMARRER",
|
"match.status.start": "DÉMARRER",
|
||||||
"matches.active.team": "Équipe active : %1$@.",
|
"matches.active.team": "Équipe active : %1$@.",
|
||||||
"matches.change": "Changer",
|
"matches.change": "Changer",
|
||||||
"matches.empty.hint": "Programmez un match ou démarrez-en un avec « Nouveau match ».",
|
"matches.empty.hint": "Touchez « Nouveau match » pour planifier à l'avance ou démarrer tout de suite.",
|
||||||
"matches.empty.title": "Aucun match au calendrier",
|
"matches.empty.title": "Aucun match au calendrier",
|
||||||
"matches.hello": "Bonjour, %1$@",
|
"matches.hello": "Bonjour, %1$@",
|
||||||
"matches.load.error": "Erreur de chargement",
|
"matches.load.error": "Erreur de chargement",
|
||||||
@@ -794,9 +816,8 @@ enum L10n {
|
|||||||
"matches.no.team.title": "Aucune équipe assignée",
|
"matches.no.team.title": "Aucune équipe assignée",
|
||||||
"matches.ready.title": "Prêts à démarrer",
|
"matches.ready.title": "Prêts à démarrer",
|
||||||
"matches.retry": "Réessayer",
|
"matches.retry": "Réessayer",
|
||||||
"matches.schedule": "Match programmé",
|
|
||||||
"matches.scheduled.title": "Matchs programmés",
|
"matches.scheduled.title": "Matchs programmés",
|
||||||
"matches.subtitle": "Reprenez un direct en cours ou démarrez un match programmé.",
|
"matches.subtitle": "Reprenez un direct en cours ou démarrez un nouveau match.",
|
||||||
"matches.tap.change.team": "Appuyez pour changer d'équipe",
|
"matches.tap.change.team": "Appuyez pour changer d'équipe",
|
||||||
"matches.team.for.live": "Équipe pour le direct",
|
"matches.team.for.live": "Équipe pour le direct",
|
||||||
"matches.title": "Matchs",
|
"matches.title": "Matchs",
|
||||||
@@ -992,6 +1013,7 @@ enum L10n {
|
|||||||
"broadcast.error.rtmp.url.invalid": "Ungültige RTMP-URL",
|
"broadcast.error.rtmp.url.invalid": "Ungültige RTMP-URL",
|
||||||
"broadcast.error.session.unavailable": "Sitzung nicht verfügbar",
|
"broadcast.error.session.unavailable": "Sitzung nicht verfügbar",
|
||||||
"broadcast.hide.controls.cd": "Steuerung ausblenden",
|
"broadcast.hide.controls.cd": "Steuerung ausblenden",
|
||||||
|
"broadcast.mute.cd": "Audio stummschalten",
|
||||||
"broadcast.next.period.cd": "Nächste Periode",
|
"broadcast.next.period.cd": "Nächste Periode",
|
||||||
"broadcast.pause.cd": "Livestream pausieren",
|
"broadcast.pause.cd": "Livestream pausieren",
|
||||||
"broadcast.period.label.timed": "Halbzeit %1$d",
|
"broadcast.period.label.timed": "Halbzeit %1$d",
|
||||||
@@ -1013,6 +1035,9 @@ enum L10n {
|
|||||||
"broadcast.side.away": "Gast",
|
"broadcast.side.away": "Gast",
|
||||||
"broadcast.side.home": "Heim",
|
"broadcast.side.home": "Heim",
|
||||||
"broadcast.snackbar.closed.remote": "Livestream von der Regie beendet",
|
"broadcast.snackbar.closed.remote": "Livestream von der Regie beendet",
|
||||||
|
"broadcast.snackbar.mute.error": "Audio: %1$@",
|
||||||
|
"broadcast.snackbar.muted": "Audio stummgeschaltet",
|
||||||
|
"broadcast.snackbar.muted.remote": "Audio von der Regie stummgeschaltet",
|
||||||
"broadcast.snackbar.pause.error": "Pause: %1$@",
|
"broadcast.snackbar.pause.error": "Pause: %1$@",
|
||||||
"broadcast.snackbar.paused": "Livestream pausiert",
|
"broadcast.snackbar.paused": "Livestream pausiert",
|
||||||
"broadcast.snackbar.paused.remote": "Von der Regie pausiert",
|
"broadcast.snackbar.paused.remote": "Von der Regie pausiert",
|
||||||
@@ -1021,6 +1046,8 @@ enum L10n {
|
|||||||
"broadcast.snackbar.resume.rtmp.error": "RTMP-Fortsetzung: %1$@",
|
"broadcast.snackbar.resume.rtmp.error": "RTMP-Fortsetzung: %1$@",
|
||||||
"broadcast.snackbar.resumed": "Livestream fortgesetzt",
|
"broadcast.snackbar.resumed": "Livestream fortgesetzt",
|
||||||
"broadcast.snackbar.resumed.remote": "Von der Regie fortgesetzt",
|
"broadcast.snackbar.resumed.remote": "Von der Regie fortgesetzt",
|
||||||
|
"broadcast.snackbar.unmuted": "Audio wieder aktiv",
|
||||||
|
"broadcast.snackbar.unmuted.remote": "Audio von der Regie wieder aktiviert",
|
||||||
"broadcast.status.connecting": "VERBINDUNG…",
|
"broadcast.status.connecting": "VERBINDUNG…",
|
||||||
"broadcast.status.error.fallback": "FEHLER",
|
"broadcast.status.error.fallback": "FEHLER",
|
||||||
"broadcast.status.live": "LIVE",
|
"broadcast.status.live": "LIVE",
|
||||||
@@ -1037,6 +1064,8 @@ enum L10n {
|
|||||||
"broadcast.tooltip.add.point": "Punkt hinzufügen %1$@",
|
"broadcast.tooltip.add.point": "Punkt hinzufügen %1$@",
|
||||||
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
||||||
"broadcast.tooltip.remove.point": "Punkt entfernen %1$@",
|
"broadcast.tooltip.remove.point": "Punkt entfernen %1$@",
|
||||||
|
"broadcast.unmute.cd": "Audio einschalten",
|
||||||
|
"broadcast.audio.muted.label": "Audio aus",
|
||||||
"common.close": "Schließen",
|
"common.close": "Schließen",
|
||||||
"common.error.generic": "Fehler",
|
"common.error.generic": "Fehler",
|
||||||
"common.error.title": "Fehler",
|
"common.error.title": "Fehler",
|
||||||
@@ -1067,7 +1096,7 @@ enum L10n {
|
|||||||
"match.status.start": "STARTEN",
|
"match.status.start": "STARTEN",
|
||||||
"matches.active.team": "Aktives Team: %1$@.",
|
"matches.active.team": "Aktives Team: %1$@.",
|
||||||
"matches.change": "Wechseln",
|
"matches.change": "Wechseln",
|
||||||
"matches.empty.hint": "Plane ein Spiel oder starte ein neues mit «Neues Spiel».",
|
"matches.empty.hint": "Tippe auf «Neues Spiel», um im Voraus zu planen oder sofort zu starten.",
|
||||||
"matches.empty.title": "Keine Spiele im Kalender",
|
"matches.empty.title": "Keine Spiele im Kalender",
|
||||||
"matches.hello": "Hallo, %1$@",
|
"matches.hello": "Hallo, %1$@",
|
||||||
"matches.load.error": "Ladefehler",
|
"matches.load.error": "Ladefehler",
|
||||||
@@ -1085,9 +1114,8 @@ enum L10n {
|
|||||||
"matches.no.team.title": "Kein Team zugewiesen",
|
"matches.no.team.title": "Kein Team zugewiesen",
|
||||||
"matches.ready.title": "Bereit zum Start",
|
"matches.ready.title": "Bereit zum Start",
|
||||||
"matches.retry": "Erneut versuchen",
|
"matches.retry": "Erneut versuchen",
|
||||||
"matches.schedule": "Geplantes Spiel",
|
|
||||||
"matches.scheduled.title": "Geplante Spiele",
|
"matches.scheduled.title": "Geplante Spiele",
|
||||||
"matches.subtitle": "Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.",
|
"matches.subtitle": "Nimm einen laufenden Livestream wieder auf oder starte ein neues Spiel.",
|
||||||
"matches.tap.change.team": "Tippen zum Teamwechsel",
|
"matches.tap.change.team": "Tippen zum Teamwechsel",
|
||||||
"matches.team.for.live": "Team für den Livestream",
|
"matches.team.for.live": "Team für den Livestream",
|
||||||
"matches.title": "Spiele",
|
"matches.title": "Spiele",
|
||||||
@@ -1283,6 +1311,7 @@ enum L10n {
|
|||||||
"broadcast.error.rtmp.url.invalid": "URL RTMP no válida",
|
"broadcast.error.rtmp.url.invalid": "URL RTMP no válida",
|
||||||
"broadcast.error.session.unavailable": "Sesión no disponible",
|
"broadcast.error.session.unavailable": "Sesión no disponible",
|
||||||
"broadcast.hide.controls.cd": "Ocultar controles",
|
"broadcast.hide.controls.cd": "Ocultar controles",
|
||||||
|
"broadcast.mute.cd": "Silenciar audio",
|
||||||
"broadcast.next.period.cd": "Periodo siguiente",
|
"broadcast.next.period.cd": "Periodo siguiente",
|
||||||
"broadcast.pause.cd": "Pausar directo",
|
"broadcast.pause.cd": "Pausar directo",
|
||||||
"broadcast.period.label.timed": "Tiempo %1$d",
|
"broadcast.period.label.timed": "Tiempo %1$d",
|
||||||
@@ -1304,6 +1333,9 @@ enum L10n {
|
|||||||
"broadcast.side.away": "visitante",
|
"broadcast.side.away": "visitante",
|
||||||
"broadcast.side.home": "local",
|
"broadcast.side.home": "local",
|
||||||
"broadcast.snackbar.closed.remote": "Directo cerrado desde regie",
|
"broadcast.snackbar.closed.remote": "Directo cerrado desde regie",
|
||||||
|
"broadcast.snackbar.mute.error": "Audio: %1$@",
|
||||||
|
"broadcast.snackbar.muted": "Audio silenciado",
|
||||||
|
"broadcast.snackbar.muted.remote": "Audio silenciado desde regie",
|
||||||
"broadcast.snackbar.pause.error": "Pausa: %1$@",
|
"broadcast.snackbar.pause.error": "Pausa: %1$@",
|
||||||
"broadcast.snackbar.paused": "Directo en pausa",
|
"broadcast.snackbar.paused": "Directo en pausa",
|
||||||
"broadcast.snackbar.paused.remote": "Pausado desde regie",
|
"broadcast.snackbar.paused.remote": "Pausado desde regie",
|
||||||
@@ -1312,6 +1344,8 @@ enum L10n {
|
|||||||
"broadcast.snackbar.resume.rtmp.error": "Reanudación RTMP: %1$@",
|
"broadcast.snackbar.resume.rtmp.error": "Reanudación RTMP: %1$@",
|
||||||
"broadcast.snackbar.resumed": "Directo reanudado",
|
"broadcast.snackbar.resumed": "Directo reanudado",
|
||||||
"broadcast.snackbar.resumed.remote": "Reanudado desde regie",
|
"broadcast.snackbar.resumed.remote": "Reanudado desde regie",
|
||||||
|
"broadcast.snackbar.unmuted": "Audio reactivado",
|
||||||
|
"broadcast.snackbar.unmuted.remote": "Audio reactivado desde regie",
|
||||||
"broadcast.status.connecting": "CONECTANDO…",
|
"broadcast.status.connecting": "CONECTANDO…",
|
||||||
"broadcast.status.error.fallback": "ERROR",
|
"broadcast.status.error.fallback": "ERROR",
|
||||||
"broadcast.status.live": "EN DIRECTO",
|
"broadcast.status.live": "EN DIRECTO",
|
||||||
@@ -1328,6 +1362,8 @@ enum L10n {
|
|||||||
"broadcast.tooltip.add.point": "Añadir punto %1$@",
|
"broadcast.tooltip.add.point": "Añadir punto %1$@",
|
||||||
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
"broadcast.tooltip.plus.side": "+%1$d %2$@",
|
||||||
"broadcast.tooltip.remove.point": "Quitar punto %1$@",
|
"broadcast.tooltip.remove.point": "Quitar punto %1$@",
|
||||||
|
"broadcast.unmute.cd": "Reactivar audio",
|
||||||
|
"broadcast.audio.muted.label": "Audio off",
|
||||||
"common.close": "Cerrar",
|
"common.close": "Cerrar",
|
||||||
"common.error.generic": "error",
|
"common.error.generic": "error",
|
||||||
"common.error.title": "Error",
|
"common.error.title": "Error",
|
||||||
@@ -1358,7 +1394,7 @@ enum L10n {
|
|||||||
"match.status.start": "INICIAR",
|
"match.status.start": "INICIAR",
|
||||||
"matches.active.team": "Equipo activo: %1$@.",
|
"matches.active.team": "Equipo activo: %1$@.",
|
||||||
"matches.change": "Cambiar",
|
"matches.change": "Cambiar",
|
||||||
"matches.empty.hint": "Programa un partido o inicia uno nuevo con «Nuevo partido».",
|
"matches.empty.hint": "Toca «Nuevo partido» para programar con antelación o empezar ya.",
|
||||||
"matches.empty.title": "Ningún partido en el calendario",
|
"matches.empty.title": "Ningún partido en el calendario",
|
||||||
"matches.hello": "Hola, %1$@",
|
"matches.hello": "Hola, %1$@",
|
||||||
"matches.load.error": "Error de carga",
|
"matches.load.error": "Error de carga",
|
||||||
@@ -1376,9 +1412,8 @@ enum L10n {
|
|||||||
"matches.no.team.title": "Ningún equipo asignado",
|
"matches.no.team.title": "Ningún equipo asignado",
|
||||||
"matches.ready.title": "Listos para empezar",
|
"matches.ready.title": "Listos para empezar",
|
||||||
"matches.retry": "Reintentar",
|
"matches.retry": "Reintentar",
|
||||||
"matches.schedule": "Partido programado",
|
|
||||||
"matches.scheduled.title": "Partidos programados",
|
"matches.scheduled.title": "Partidos programados",
|
||||||
"matches.subtitle": "Reanuda un directo en curso o inicia un partido programado.",
|
"matches.subtitle": "Reanuda un directo en curso o inicia un nuevo partido.",
|
||||||
"matches.tap.change.team": "Toca para cambiar de equipo",
|
"matches.tap.change.team": "Toca para cambiar de equipo",
|
||||||
"matches.team.for.live": "Equipo para el directo",
|
"matches.team.for.live": "Equipo para el directo",
|
||||||
"matches.title": "Partidos",
|
"matches.title": "Partidos",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ struct StreamSessionDto: Decodable {
|
|||||||
let targetFps: Int?
|
let targetFps: Int?
|
||||||
let qualityPreset: String?
|
let qualityPreset: String?
|
||||||
let startedAt: String?
|
let startedAt: String?
|
||||||
|
let audioMuted: Bool?
|
||||||
let score: ScoreStateDto?
|
let score: ScoreStateDto?
|
||||||
|
|
||||||
func toDomain() -> StreamSession {
|
func toDomain() -> StreamSession {
|
||||||
@@ -222,7 +223,8 @@ struct StreamSessionDto: Decodable {
|
|||||||
targetFps: targetFps ?? 30,
|
targetFps: targetFps ?? 30,
|
||||||
qualityPreset: qualityPreset ?? "720p_30_2.5mbps",
|
qualityPreset: qualityPreset ?? "720p_30_2.5mbps",
|
||||||
startedAt: startedAt,
|
startedAt: startedAt,
|
||||||
score: score?.toDomain()
|
score: score?.toDomain(),
|
||||||
|
audioMuted: audioMuted ?? false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,6 +416,10 @@ struct CreateSessionRequest: Encodable {
|
|||||||
let youtubeChannel: String?
|
let youtubeChannel: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AudioMuteRequest: Encodable {
|
||||||
|
let muted: Bool
|
||||||
|
}
|
||||||
|
|
||||||
struct ScoringRulesBody: Codable {
|
struct ScoringRulesBody: Codable {
|
||||||
let setsToWin: Int?
|
let setsToWin: Int?
|
||||||
let pointsPerSet: Int?
|
let pointsPerSet: Int?
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ final class MatchLiveAPI: @unchecked Sendable {
|
|||||||
try await patch("sessions/\(id)/resume", body: EmptyBody())
|
try await patch("sessions/\(id)/resume", body: EmptyBody())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setAudioMute(id: String, muted: Bool) async throws -> StreamSessionDto {
|
||||||
|
try await patch("sessions/\(id)/audio_mute", body: AudioMuteRequest(muted: muted))
|
||||||
|
}
|
||||||
|
|
||||||
func networkTest(sessionId: String, body: NetworkTestRequest) async throws -> NetworkTestResponse {
|
func networkTest(sessionId: String, body: NetworkTestRequest) async throws -> NetworkTestResponse {
|
||||||
try await post("sessions/\(sessionId)/network_test", body: body)
|
try await post("sessions/\(sessionId)/network_test", body: body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ final class SessionCableService: ObservableObject {
|
|||||||
var onPauseStream: (() -> Void)?
|
var onPauseStream: (() -> Void)?
|
||||||
var onResumeStream: (() -> Void)?
|
var onResumeStream: (() -> Void)?
|
||||||
var onStopStream: (() -> Void)?
|
var onStopStream: (() -> Void)?
|
||||||
|
var onAudioMute: ((Bool) -> Void)?
|
||||||
|
|
||||||
private var client: ActionCableClient?
|
private var client: ActionCableClient?
|
||||||
private var intentionalDisconnect = false
|
private var intentionalDisconnect = false
|
||||||
@@ -89,6 +90,9 @@ extension SessionCableService: ActionCableClient.Listener {
|
|||||||
onScoreUpdate?(ScoreState.fromCablePayload(payload))
|
onScoreUpdate?(ScoreState.fromCablePayload(payload))
|
||||||
case "command":
|
case "command":
|
||||||
let action = payload["action"] as? String ?? ""
|
let action = payload["action"] as? String ?? ""
|
||||||
|
if let muted = AudioMuteCommandLogic.muted(type: "command", action: action, event: nil, muted: payload["muted"]) {
|
||||||
|
onAudioMute?(muted)
|
||||||
|
}
|
||||||
switch action {
|
switch action {
|
||||||
case "pause_stream": onPauseStream?()
|
case "pause_stream": onPauseStream?()
|
||||||
case "resume_stream": onResumeStream?()
|
case "resume_stream": onResumeStream?()
|
||||||
@@ -97,6 +101,9 @@ extension SessionCableService: ActionCableClient.Listener {
|
|||||||
}
|
}
|
||||||
case "stream_event":
|
case "stream_event":
|
||||||
let event = payload["event"] as? String ?? ""
|
let event = payload["event"] as? String ?? ""
|
||||||
|
if let muted = AudioMuteCommandLogic.muted(type: "stream_event", action: nil, event: event, muted: payload["muted"]) {
|
||||||
|
onAudioMute?(muted)
|
||||||
|
}
|
||||||
switch event {
|
switch event {
|
||||||
case "paused": onPauseStream?()
|
case "paused": onPauseStream?()
|
||||||
case "resumed": onResumeStream?()
|
case "resumed": onResumeStream?()
|
||||||
@@ -109,3 +116,23 @@ extension SessionCableService: ActionCableClient.Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AudioMuteCommandLogic {
|
||||||
|
static func muted(type: String, action: String?, event: String?, muted: Any?) -> Bool? {
|
||||||
|
switch type {
|
||||||
|
case "command":
|
||||||
|
switch action {
|
||||||
|
case "mute_audio": return true
|
||||||
|
case "unmute_audio": return false
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
case "stream_event":
|
||||||
|
guard event == "audio_muted" else { return nil }
|
||||||
|
if let value = muted as? Bool { return value }
|
||||||
|
if let value = muted as? String { return value == "true" }
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ final class SessionRepository {
|
|||||||
try await api.resumeSession(id: id).toDomain()
|
try await api.resumeSession(id: id).toDomain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setAudioMute(id: String, muted: Bool) async throws -> StreamSession {
|
||||||
|
try await api.setAudioMute(id: id, muted: muted).toDomain()
|
||||||
|
}
|
||||||
|
|
||||||
func submitNetworkTest(
|
func submitNetworkTest(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
downloadMbps: Double,
|
downloadMbps: Double,
|
||||||
|
|||||||
@@ -121,10 +121,33 @@ struct StreamSession: Identifiable, Equatable, Sendable {
|
|||||||
let qualityPreset: String
|
let qualityPreset: String
|
||||||
let startedAt: String?
|
let startedAt: String?
|
||||||
let score: ScoreState?
|
let score: ScoreState?
|
||||||
|
let audioMuted: Bool
|
||||||
|
|
||||||
var isLive: Bool { status == "live" || status == "reconnecting" }
|
var isLive: Bool { status == "live" || status == "reconnecting" }
|
||||||
var isPaused: Bool { status == "paused" }
|
var isPaused: Bool { status == "paused" }
|
||||||
|
|
||||||
|
func withAudioMuted(_ muted: Bool) -> StreamSession {
|
||||||
|
StreamSession(
|
||||||
|
id: id,
|
||||||
|
matchId: matchId,
|
||||||
|
status: status,
|
||||||
|
platform: platform,
|
||||||
|
rtmpIngestUrl: rtmpIngestUrl,
|
||||||
|
hlsPlaybackUrl: hlsPlaybackUrl,
|
||||||
|
watchPageUrl: watchPageUrl,
|
||||||
|
shareUrl: shareUrl,
|
||||||
|
youtubeWatchUrl: youtubeWatchUrl,
|
||||||
|
youtubeReady: youtubeReady,
|
||||||
|
privacyStatus: privacyStatus,
|
||||||
|
targetBitrate: targetBitrate,
|
||||||
|
targetFps: targetFps,
|
||||||
|
qualityPreset: qualityPreset,
|
||||||
|
startedAt: startedAt,
|
||||||
|
score: score,
|
||||||
|
audioMuted: muted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func watchShareUrl() -> String? {
|
func watchShareUrl() -> String? {
|
||||||
if platform == "youtube", let url = youtubeWatchUrl, !url.isEmpty { return url }
|
if platform == "youtube", let url = youtubeWatchUrl, !url.isEmpty { return url }
|
||||||
if let url = shareUrl, !url.isEmpty { return url }
|
if let url = shareUrl, !url.isEmpty { return url }
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 216 KiB |
@@ -19,7 +19,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.0.5</string>
|
<string>2.0.10</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>26</string>
|
<string>32</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
@@ -55,6 +55,14 @@
|
|||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ struct BroadcastMetrics: Sendable {
|
|||||||
var fps: Int = 0
|
var fps: Int = 0
|
||||||
var connected: Bool = false
|
var connected: Bool = false
|
||||||
var lastError: String?
|
var lastError: String?
|
||||||
|
var audioMuted: Bool = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ final class LiveBroadcastCoordinator: ObservableObject {
|
|||||||
engine.updateOverlay(state)
|
engine.updateOverlay(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setAudioMuted(_ muted: Bool) async {
|
||||||
|
await engine.setAudioMuted(muted)
|
||||||
|
}
|
||||||
|
|
||||||
func configureBackgroundAudio() {
|
func configureBackgroundAudio() {
|
||||||
let session = AVAudioSession.sharedInstance()
|
let session = AVAudioSession.sharedInstance()
|
||||||
try? session.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .allowBluetooth])
|
try? session.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .allowBluetooth])
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ final class LiveBroadcastEngine: ObservableObject {
|
|||||||
private var lastAppliedCaptureOrientation: AVCaptureVideoOrientation?
|
private var lastAppliedCaptureOrientation: AVCaptureVideoOrientation?
|
||||||
private var orientationDebounceTask: Task<Void, Never>?
|
private var orientationDebounceTask: Task<Void, Never>?
|
||||||
private var rtmpSession: (any Session)?
|
private var rtmpSession: (any Session)?
|
||||||
|
private var audioMuted = false
|
||||||
private static var rtmpFactoryRegistered = false
|
private static var rtmpFactoryRegistered = false
|
||||||
|
|
||||||
private static func ensureRTMPFactoryRegistered() async {
|
private static func ensureRTMPFactoryRegistered() async {
|
||||||
@@ -173,12 +174,21 @@ final class LiveBroadcastEngine: ObservableObject {
|
|||||||
overlayRenderer.detach()
|
overlayRenderer.detach()
|
||||||
try? await mixer.stopRunning()
|
try? await mixer.stopRunning()
|
||||||
pipelineConfigured = false
|
pipelineConfigured = false
|
||||||
|
audioMuted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func release() async {
|
func release() async {
|
||||||
await stopBroadcast()
|
await stopBroadcast()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Silenzia il microfono senza togliere la traccia AAC (YouTube/MediaMTX richiedono audio continuo).
|
||||||
|
func setAudioMuted(_ muted: Bool) async {
|
||||||
|
audioMuted = muted
|
||||||
|
await applyAudioMute()
|
||||||
|
metrics.audioMuted = muted
|
||||||
|
emitMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
/// Adattamento termico: aggiorna profilo video senza interrompere la sessione RTMP attiva.
|
/// Adattamento termico: aggiorna profilo video senza interrompere la sessione RTMP attiva.
|
||||||
func applyThermalProfile(videoBitrate: Int, fps: Int) async {
|
func applyThermalProfile(videoBitrate: Int, fps: Int) async {
|
||||||
guard var current = config else { return }
|
guard var current = config else { return }
|
||||||
@@ -219,7 +229,8 @@ final class LiveBroadcastEngine: ObservableObject {
|
|||||||
AudioMixerSettings(
|
AudioMixerSettings(
|
||||||
sampleRate: BroadcastConfig.audioSampleRateHz,
|
sampleRate: BroadcastConfig.audioSampleRateHz,
|
||||||
channels: BroadcastConfig.audioChannels,
|
channels: BroadcastConfig.audioChannels,
|
||||||
tracks: [0: AudioMixerTrackSettings(downmix: true, channelMap: [0])]
|
isMuted: audioMuted,
|
||||||
|
tracks: [0: AudioMixerTrackSettings(isMuted: audioMuted, downmix: true, channelMap: [0])]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
|
if let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
|
||||||
@@ -324,7 +335,8 @@ final class LiveBroadcastEngine: ObservableObject {
|
|||||||
bitrateKbps: kbps,
|
bitrateKbps: kbps,
|
||||||
fps: self.config?.fps ?? self.metrics.fps,
|
fps: self.config?.fps ?? self.metrics.fps,
|
||||||
connected: self.phase == .live,
|
connected: self.phase == .live,
|
||||||
lastError: self.metrics.lastError
|
lastError: self.metrics.lastError,
|
||||||
|
audioMuted: self.audioMuted
|
||||||
)
|
)
|
||||||
self.metricsListener?(self.metrics)
|
self.metricsListener?(self.metrics)
|
||||||
}
|
}
|
||||||
@@ -496,6 +508,16 @@ final class LiveBroadcastEngine: ObservableObject {
|
|||||||
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applyAudioMute() async {
|
||||||
|
guard pipelineConfigured else { return }
|
||||||
|
var settings = await mixer.audioMixerSettings
|
||||||
|
settings.isMuted = audioMuted
|
||||||
|
var track = settings.tracks[0] ?? AudioMixerTrackSettings(downmix: true, channelMap: [0])
|
||||||
|
track.isMuted = audioMuted
|
||||||
|
settings.tracks[0] = track
|
||||||
|
try? await mixer.setAudioMixerSettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
private func setPhase(_ newPhase: BroadcastPhase, error: String? = nil) {
|
private func setPhase(_ newPhase: BroadcastPhase, error: String? = nil) {
|
||||||
phase = newPhase
|
phase = newPhase
|
||||||
metrics = BroadcastMetrics(
|
metrics = BroadcastMetrics(
|
||||||
@@ -503,7 +525,8 @@ final class LiveBroadcastEngine: ObservableObject {
|
|||||||
bitrateKbps: metrics.bitrateKbps,
|
bitrateKbps: metrics.bitrateKbps,
|
||||||
fps: config?.fps ?? metrics.fps,
|
fps: config?.fps ?? metrics.fps,
|
||||||
connected: newPhase == .live,
|
connected: newPhase == .live,
|
||||||
lastError: error
|
lastError: error,
|
||||||
|
audioMuted: audioMuted
|
||||||
)
|
)
|
||||||
metricsListener?(metrics)
|
metricsListener?(metrics)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ struct BroadcastControlsOverlay: View {
|
|||||||
var onCloseSet: (() -> Void)?
|
var onCloseSet: (() -> Void)?
|
||||||
var onAdvancePeriod: (() -> Void)?
|
var onAdvancePeriod: (() -> Void)?
|
||||||
let onPauseOrResume: () -> Void
|
let onPauseOrResume: () -> Void
|
||||||
|
let onToggleAudioMute: () -> Void
|
||||||
|
let audioMuted: Bool
|
||||||
let onTerminate: () -> Void
|
let onTerminate: () -> Void
|
||||||
let onShareLive: () -> Void
|
let onShareLive: () -> Void
|
||||||
let onShareRegia: () -> Void
|
let onShareRegia: () -> Void
|
||||||
@@ -69,7 +71,8 @@ struct BroadcastControlsOverlay: View {
|
|||||||
targetFps: targetFps,
|
targetFps: targetFps,
|
||||||
bitrateKbps: bitrateKbps,
|
bitrateKbps: bitrateKbps,
|
||||||
networkType: networkType,
|
networkType: networkType,
|
||||||
deviceHealth: deviceHealth
|
deviceHealth: deviceHealth,
|
||||||
|
audioMuted: audioMuted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(.trailing, 8)
|
.padding(.trailing, 8)
|
||||||
@@ -142,6 +145,12 @@ struct BroadcastControlsOverlay: View {
|
|||||||
action: onPauseOrResume,
|
action: onPauseOrResume,
|
||||||
highlighted: isPaused
|
highlighted: isPaused
|
||||||
)
|
)
|
||||||
|
SideIconButton(
|
||||||
|
systemName: audioMuted ? "speaker.slash.fill" : "speaker.wave.2.fill",
|
||||||
|
accessibilityLabel: audioMuted ? L10n.t("broadcast.unmute.cd") : L10n.t("broadcast.mute.cd"),
|
||||||
|
action: onToggleAudioMute,
|
||||||
|
highlighted: audioMuted
|
||||||
|
)
|
||||||
SideIconButton(
|
SideIconButton(
|
||||||
systemName: "stop.fill",
|
systemName: "stop.fill",
|
||||||
accessibilityLabel: L10n.t("broadcast.terminate.cd"),
|
accessibilityLabel: L10n.t("broadcast.terminate.cd"),
|
||||||
@@ -195,6 +204,7 @@ private struct BroadcastTelemetryPanel: View {
|
|||||||
let bitrateKbps: Int
|
let bitrateKbps: Int
|
||||||
let networkType: String
|
let networkType: String
|
||||||
let deviceHealth: DeviceHealth
|
let deviceHealth: DeviceHealth
|
||||||
|
let audioMuted: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
@@ -214,6 +224,11 @@ private struct BroadcastTelemetryPanel: View {
|
|||||||
Text("\(networkType) · \(deviceHealth.batteryPercent)%")
|
Text("\(networkType) · \(deviceHealth.batteryPercent)%")
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 11, weight: .medium))
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
|
if audioMuted {
|
||||||
|
Text(L10n.t("broadcast.audio.muted.label"))
|
||||||
|
.font(.system(size: 11, weight: .bold))
|
||||||
|
.foregroundStyle(MatchColors.accentYellow)
|
||||||
|
}
|
||||||
ThermalIndicator(state: deviceHealth.thermalState)
|
ThermalIndicator(state: deviceHealth.thermalState)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct BroadcastScreen: View {
|
|||||||
@State private var logoReady = 0
|
@State private var logoReady = 0
|
||||||
@State private var pauseInFlight = false
|
@State private var pauseInFlight = false
|
||||||
@State private var resumeInFlight = false
|
@State private var resumeInFlight = false
|
||||||
|
@State private var muteInFlight = false
|
||||||
@State private var deviceHealth = DeviceTelemetry.snapshot()
|
@State private var deviceHealth = DeviceTelemetry.snapshot()
|
||||||
@State private var snackbarMessage: String?
|
@State private var snackbarMessage: String?
|
||||||
@State private var shareItem: ShareItem?
|
@State private var shareItem: ShareItem?
|
||||||
@@ -188,6 +189,7 @@ struct BroadcastScreen: View {
|
|||||||
let metrics = broadcastCoordinator.metrics
|
let metrics = broadcastCoordinator.metrics
|
||||||
let score = scoreController.score
|
let score = scoreController.score
|
||||||
let isPaused = session.isPaused || metrics.phase == .paused
|
let isPaused = session.isPaused || metrics.phase == .paused
|
||||||
|
let audioMuted = session.audioMuted || metrics.audioMuted
|
||||||
let statusText = broadcastStatusText(isPaused: isPaused, metrics: metrics)
|
let statusText = broadcastStatusText(isPaused: isPaused, metrics: metrics)
|
||||||
let statusColor = broadcastStatusColor(isPaused: isPaused, metrics: metrics)
|
let statusColor = broadcastStatusColor(isPaused: isPaused, metrics: metrics)
|
||||||
let boardType = match.boardType.isEmpty ? score.boardType : match.boardType
|
let boardType = match.boardType.isEmpty ? score.boardType : match.boardType
|
||||||
@@ -282,6 +284,10 @@ struct BroadcastScreen: View {
|
|||||||
pauseStream()
|
pauseStream()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onToggleAudioMute: {
|
||||||
|
Task { await setAudioMuted(!audioMuted, remote: false) }
|
||||||
|
},
|
||||||
|
audioMuted: audioMuted,
|
||||||
onTerminate: { Task { await stopStream() } },
|
onTerminate: { Task { await stopStream() } },
|
||||||
onShareLive: { shareLiveLink(session: session, subject: shareSubject) },
|
onShareLive: { shareLiveLink(session: session, subject: shareSubject) },
|
||||||
onShareRegia: { shareRegiaLink(subject: shareSubject) },
|
onShareRegia: { shareRegiaLink(subject: shareSubject) },
|
||||||
@@ -384,6 +390,7 @@ struct BroadcastScreen: View {
|
|||||||
config: config,
|
config: config,
|
||||||
paused: loaded.isPaused
|
paused: loaded.isPaused
|
||||||
)
|
)
|
||||||
|
await container.broadcastCoordinator.setAudioMuted(loaded.audioMuted)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
guard generation == bootstrapGeneration else { return }
|
guard generation == bootstrapGeneration else { return }
|
||||||
@@ -418,6 +425,12 @@ struct BroadcastScreen: View {
|
|||||||
container.sessionCable.onStopStream = { [weak container] in
|
container.sessionCable.onStopStream = { [weak container] in
|
||||||
Task { @MainActor in await stopStream() }
|
Task { @MainActor in await stopStream() }
|
||||||
}
|
}
|
||||||
|
container.sessionCable.onAudioMute = { muted in
|
||||||
|
Task { @MainActor in
|
||||||
|
guard !muteInFlight else { return }
|
||||||
|
await setAudioMuted(muted, remote: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
container.sessionCable.connect(sessionId: sessionId, accessToken: token)
|
container.sessionCable.connect(sessionId: sessionId, accessToken: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +511,7 @@ struct BroadcastScreen: View {
|
|||||||
try await container.broadcastCoordinator.resumeBroadcast(config: config)
|
try await container.broadcastCoordinator.resumeBroadcast(config: config)
|
||||||
updated = try await container.sessionRepository.fetchSession(id: sessionId)
|
updated = try await container.sessionRepository.fetchSession(id: sessionId)
|
||||||
session = updated
|
session = updated
|
||||||
|
await container.broadcastCoordinator.setAudioMuted(updated.audioMuted)
|
||||||
snackbarMessage = L10n.t("broadcast.snackbar.resumed")
|
snackbarMessage = L10n.t("broadcast.snackbar.resumed")
|
||||||
} catch {
|
} catch {
|
||||||
snackbarMessage = L10n.t("broadcast.snackbar.resume.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
|
snackbarMessage = L10n.t("broadcast.snackbar.resume.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
|
||||||
@@ -516,6 +530,31 @@ struct BroadcastScreen: View {
|
|||||||
snackbarMessage = L10n.t("broadcast.snackbar.paused.remote")
|
snackbarMessage = L10n.t("broadcast.snackbar.paused.remote")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setAudioMuted(_ muted: Bool, remote: Bool) async {
|
||||||
|
guard !muteInFlight else { return }
|
||||||
|
if (session?.audioMuted == true || broadcastCoordinator.metrics.audioMuted) == muted {
|
||||||
|
await container.broadcastCoordinator.setAudioMuted(muted)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
muteInFlight = true
|
||||||
|
defer { muteInFlight = false }
|
||||||
|
do {
|
||||||
|
if remote {
|
||||||
|
session = session?.withAudioMuted(muted)
|
||||||
|
} else {
|
||||||
|
session = try await container.sessionRepository.setAudioMute(id: sessionId, muted: muted)
|
||||||
|
}
|
||||||
|
await container.broadcastCoordinator.setAudioMuted(muted)
|
||||||
|
snackbarMessage = L10n.t(
|
||||||
|
remote
|
||||||
|
? (muted ? "broadcast.snackbar.muted.remote" : "broadcast.snackbar.unmuted.remote")
|
||||||
|
: (muted ? "broadcast.snackbar.muted" : "broadcast.snackbar.unmuted")
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
snackbarMessage = L10n.t("broadcast.snackbar.mute.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Ripresa dalla regia / echo cable: solo RTMP locale, senza PATCH resume.
|
/// Ripresa dalla regia / echo cable: solo RTMP locale, senza PATCH resume.
|
||||||
private func applyRemoteResume(container: AppContainer) async {
|
private func applyRemoteResume(container: AppContainer) async {
|
||||||
guard !resumeInFlight else { return }
|
guard !resumeInFlight else { return }
|
||||||
@@ -528,7 +567,8 @@ struct BroadcastScreen: View {
|
|||||||
let config = broadcastConfig(for: current, rtmpUrl: url)
|
let config = broadcastConfig(for: current, rtmpUrl: url)
|
||||||
thermalManager.updateBaseline(config)
|
thermalManager.updateBaseline(config)
|
||||||
try await container.broadcastCoordinator.resumeBroadcast(config: config)
|
try await container.broadcastCoordinator.resumeBroadcast(config: config)
|
||||||
snackbarMessage = L10n.t("broadcast.snackbar.resumed")
|
await container.broadcastCoordinator.setAudioMuted(current.audioMuted)
|
||||||
|
snackbarMessage = L10n.t("broadcast.snackbar.resumed.remote")
|
||||||
} catch {
|
} catch {
|
||||||
snackbarMessage = L10n.t("broadcast.snackbar.resume.rtmp.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
|
snackbarMessage = L10n.t("broadcast.snackbar.resume.rtmp.error", UserFacingError.message(for: error) ?? L10n.t("common.error.generic"))
|
||||||
}
|
}
|
||||||
@@ -567,6 +607,7 @@ struct BroadcastScreen: View {
|
|||||||
container.sessionCable.onPauseStream = nil
|
container.sessionCable.onPauseStream = nil
|
||||||
container.sessionCable.onResumeStream = nil
|
container.sessionCable.onResumeStream = nil
|
||||||
container.sessionCable.onStopStream = nil
|
container.sessionCable.onStopStream = nil
|
||||||
|
container.sessionCable.onAudioMute = nil
|
||||||
container.sessionCable.disconnect()
|
container.sessionCable.disconnect()
|
||||||
await container.broadcastCoordinator.stopBroadcast(sessionId: sessionId)
|
await container.broadcastCoordinator.stopBroadcast(sessionId: sessionId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct MatchPrimaryButton: View {
|
|||||||
.font(MatchTypography.labelLarge)
|
.font(MatchTypography.labelLarge)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.7)
|
.minimumScaleFactor(0.7)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
@@ -21,5 +21,8 @@ struct MatchScreenScaffold<Content: View, TopBar: View>: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.background(MatchColors.background)
|
.background(MatchColors.background)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
// Chrome custom: nasconde la navigation bar di sistema (evita chevron indietro spurî).
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct MatchSecondaryButton: View {
|
|||||||
.font(MatchTypography.labelLarge)
|
.font(MatchTypography.labelLarge)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.minimumScaleFactor(0.7)
|
.minimumScaleFactor(0.7)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 52)
|
.frame(height: 52)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ struct MatchesScreen: View {
|
|||||||
Text(L10n.t("matches.subtitle"))
|
Text(L10n.t("matches.subtitle"))
|
||||||
.font(MatchTypography.bodyMedium)
|
.font(MatchTypography.bodyMedium)
|
||||||
.foregroundStyle(MatchColors.textSecondary)
|
.foregroundStyle(MatchColors.textSecondary)
|
||||||
HStack(spacing: 12) {
|
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
||||||
MatchSecondaryButton(label: L10n.t("matches.schedule").uppercased(), action: { showSchedule = true })
|
|
||||||
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
|
|
||||||
}
|
|
||||||
if let activeTeam {
|
if let activeTeam {
|
||||||
TeamPickerBar(
|
TeamPickerBar(
|
||||||
team: activeTeam,
|
team: activeTeam,
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AppNavHost: View {
|
struct AppNavHost: View {
|
||||||
|
private enum RootScreen {
|
||||||
|
case splash
|
||||||
|
case login
|
||||||
|
case matches
|
||||||
|
}
|
||||||
|
|
||||||
@StateObject private var container = AppContainer()
|
@StateObject private var container = AppContainer()
|
||||||
|
@State private var root: RootScreen = .splash
|
||||||
|
/// Solo destinazioni sopra la root (account, recupero password).
|
||||||
@State private var path: [Routes] = []
|
@State private var path: [Routes] = []
|
||||||
@State private var wizardRoute: WizardRoute?
|
@State private var wizardRoute: WizardRoute?
|
||||||
@State private var broadcastRoute: BroadcastRoute?
|
@State private var broadcastRoute: BroadcastRoute?
|
||||||
@@ -17,49 +25,27 @@ struct AppNavHost: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack(path: $path) {
|
NavigationStack(path: $path) {
|
||||||
SplashScreen(
|
rootContent
|
||||||
container: container,
|
.navigationDestination(for: Routes.self) { route in
|
||||||
onAuthenticated: { path = [.matches] },
|
switch route {
|
||||||
onUnauthenticated: { path = [.login] }
|
case .forgotPassword:
|
||||||
)
|
ForgotPasswordScreen(
|
||||||
.navigationDestination(for: Routes.self) { route in
|
container: container,
|
||||||
switch route {
|
onBack: { path.removeLast() }
|
||||||
case .splash:
|
)
|
||||||
EmptyView()
|
case .account:
|
||||||
case .login:
|
AccountScreen(
|
||||||
LoginScreen(
|
container: container,
|
||||||
container: container,
|
onBack: { path.removeLast() },
|
||||||
onLoggedIn: { path = [.matches] },
|
onLoggedOut: {
|
||||||
onForgotPassword: { path.append(.forgotPassword) }
|
path = []
|
||||||
)
|
root = .login
|
||||||
case .forgotPassword:
|
}
|
||||||
ForgotPasswordScreen(
|
)
|
||||||
container: container,
|
case .splash, .login, .matches, .setup, .broadcast:
|
||||||
onBack: { path.removeLast() }
|
EmptyView()
|
||||||
)
|
}
|
||||||
case .matches:
|
|
||||||
MatchesScreen(
|
|
||||||
container: container,
|
|
||||||
refreshToken: matchesRefreshToken,
|
|
||||||
onOpenSetup: { matchId in
|
|
||||||
wizardRoute = WizardRoute(matchId: matchId, step: 1)
|
|
||||||
},
|
|
||||||
onOpenBroadcast: { sessionId in
|
|
||||||
broadcastRoute = BroadcastRoute(sessionId: sessionId)
|
|
||||||
},
|
|
||||||
onOpenAccount: { path.append(.account) }
|
|
||||||
)
|
|
||||||
.lockPortraitOrientation()
|
|
||||||
case .account:
|
|
||||||
AccountScreen(
|
|
||||||
container: container,
|
|
||||||
onBack: { path.removeLast() },
|
|
||||||
onLoggedOut: { path = [.login] }
|
|
||||||
)
|
|
||||||
case .setup, .broadcast:
|
|
||||||
EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.environment(\.locale, appLocale)
|
.environment(\.locale, appLocale)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
|
||||||
@@ -98,7 +84,8 @@ struct AppNavHost: View {
|
|||||||
AppOrientation.lockPortrait()
|
AppOrientation.lockPortrait()
|
||||||
container.wizardSession.reset()
|
container.wizardSession.reset()
|
||||||
broadcastRoute = nil
|
broadcastRoute = nil
|
||||||
path = [.matches]
|
path = []
|
||||||
|
root = .matches
|
||||||
}
|
}
|
||||||
.keepScreenOn()
|
.keepScreenOn()
|
||||||
}
|
}
|
||||||
@@ -109,4 +96,44 @@ struct AppNavHost: View {
|
|||||||
}
|
}
|
||||||
.environmentObject(container)
|
.environmentObject(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var rootContent: some View {
|
||||||
|
switch root {
|
||||||
|
case .splash:
|
||||||
|
SplashScreen(
|
||||||
|
container: container,
|
||||||
|
onAuthenticated: {
|
||||||
|
path = []
|
||||||
|
root = .matches
|
||||||
|
},
|
||||||
|
onUnauthenticated: {
|
||||||
|
path = []
|
||||||
|
root = .login
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case .login:
|
||||||
|
LoginScreen(
|
||||||
|
container: container,
|
||||||
|
onLoggedIn: {
|
||||||
|
path = []
|
||||||
|
root = .matches
|
||||||
|
},
|
||||||
|
onForgotPassword: { path.append(.forgotPassword) }
|
||||||
|
)
|
||||||
|
case .matches:
|
||||||
|
MatchesScreen(
|
||||||
|
container: container,
|
||||||
|
refreshToken: matchesRefreshToken,
|
||||||
|
onOpenSetup: { matchId in
|
||||||
|
wizardRoute = WizardRoute(matchId: matchId, step: 1)
|
||||||
|
},
|
||||||
|
onOpenBroadcast: { sessionId in
|
||||||
|
broadcastRoute = BroadcastRoute(sessionId: sessionId)
|
||||||
|
},
|
||||||
|
onOpenAccount: { path.append(.account) }
|
||||||
|
)
|
||||||
|
.lockPortraitOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,12 @@ final class LiveBroadcastCoordinatorTests: XCTestCase {
|
|||||||
func testShouldStopPreviousSessionTrueWhenSwitching() {
|
func testShouldStopPreviousSessionTrueWhenSwitching() {
|
||||||
XCTAssertTrue(LiveBroadcastCoordinator.shouldStopPreviousSession(active: "session-a", incoming: "session-b"))
|
XCTAssertTrue(LiveBroadcastCoordinator.shouldStopPreviousSession(active: "session-a", incoming: "session-b"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAudioMuteCommandFromCable() {
|
||||||
|
XCTAssertEqual(AudioMuteCommandLogic.muted(type: "command", action: "mute_audio", event: nil, muted: nil), true)
|
||||||
|
XCTAssertEqual(AudioMuteCommandLogic.muted(type: "command", action: "unmute_audio", event: nil, muted: nil), false)
|
||||||
|
XCTAssertNil(AudioMuteCommandLogic.muted(type: "command", action: "pause_stream", event: nil, muted: nil))
|
||||||
|
XCTAssertEqual(AudioMuteCommandLogic.muted(type: "stream_event", action: nil, event: "audio_muted", muted: true), true)
|
||||||
|
XCTAssertEqual(AudioMuteCommandLogic.muted(type: "stream_event", action: nil, event: "audio_muted", muted: false), false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ lines += [
|
|||||||
app_settings = """
|
app_settings = """
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 32;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
@@ -107,7 +107,7 @@ app_settings = """
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.0.5;
|
MARKETING_VERSION = 2.0.10;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
API_BASE_URL = "https://www.matchlivetv.it";
|
API_BASE_URL = "https://www.matchlivetv.it";
|
||||||
@@ -122,10 +122,10 @@ app_debug_settings = app_settings + """
|
|||||||
test_settings = """
|
test_settings = """
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 32;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
MARKETING_VERSION = 2.0.5;
|
MARKETING_VERSION = 2.0.10;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
Reference in New Issue
Block a user