Files
MatchLiveTv/backend/app/views/public/live/show.html.erb
eminuxandCursor b88f44ab1c Completa i18n IT/EN/FR/DE/ES su sito pubblico, area autenticata e admin.
Tutte le view marketing, legali, viewer, dashboard e admin usano t(); mailer utente-facing e flash localizzati; admin con LocaleResolver e selettore lingua.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 00:38:50 +02:00

342 lines
12 KiB
ERB

<% club_name = @match.team.club&.name %>
<% content_for :title do %><%= t("live.show.title", club_name: club_name, team_name: @match.team.name, opponent_name: @match.opponent_name) %><% end %>
<% content_for :meta_description do %><%= t(
"live.show.meta_description",
club_name: club_name,
team_name: @match.team.name,
opponent_name: @match.opponent_name,
location: @match.location.present? ? t("live.show.meta_description_location", location: @match.location) : ""
) %><% end %>
<% content_for :robots, @session.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
<% content_for :head do %>
<% unless @stream_closed || @session.platform == "youtube" %>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7"></script>
<% end %>
<% end %>
<div class="wrap">
<%= link_to t("live.show.back_to_all"), public_live_index_path, class: "back-link" %>
<%= live_match_page_heading(@match) %>
<% if @stream_closed %>
<div id="stream-ended" class="stream-ended" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2><%= t("live.show.stream_ended_title") %></h2>
<p><%= t("live.show.stream_ended_body") %></p>
<% if @session.ended_at %>
<p class="stream-ended-meta"><%= t("live.show.stream_ended_closed_at", date: l_local(@session.ended_at)) %></p>
<% end %>
<p class="stream-ended-hint"><%= t("live.show.stream_ended_hint") %></p>
<%= link_to t("live.show.all_live_link"), public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
</div>
<% elsif @session.platform == "youtube" %>
<div class="stream-ended youtube-live-cta" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true"></div>
<h2><%= t("live.show.youtube_title") %></h2>
<% if @session.youtube_watch_url.present? %>
<p><%= t("live.show.youtube_available_body") %></p>
<%= link_to t("live.show.youtube_watch_link"), @session.youtube_watch_url, class: "btn stream-ended-btn", target: "_blank", rel: "noopener" %>
<% else %>
<p><%= t("live.show.youtube_preparing_body") %></p>
<meta http-equiv="refresh" content="8">
<% end %>
<p class="stream-ended-hint"><%= t("live.show.youtube_hint") %></p>
</div>
<% else %>
<div class="live-player-wrap">
<video id="player" controls playsinline muted autoplay poster="/images/copertina-canale.png"></video>
<%= render "public/live/player_overlays", match: @match, session: @session %>
<button type="button" id="play-hint" class="live-play-hint" hidden>
<%= t("live.show.play_hint") %>
</button>
</div>
<p id="offline-msg" class="live-status-msg" hidden><%= t("live.show.offline_msg") %></p>
<p id="awaiting-msg" class="live-status-msg" hidden><%= t("live.show.awaiting_msg") %></p>
<p id="paused-msg" class="live-status-msg" hidden><%= t("live.show.paused_msg") %></p>
<script>
const sessionId = "<%= @session.id %>";
const hlsUrl = "<%= j @session.hls_playback_url %>";
const video = document.getElementById("player");
const offlineMsg = document.getElementById("offline-msg");
const pausedMsg = document.getElementById("paused-msg");
const awaitingMsg = document.getElementById("awaiting-msg");
const playHint = document.getElementById("play-hint");
const streamBadge = document.getElementById("live-stream-badge");
function syncStreamBadge(data) {
if (!streamBadge) return;
if (data.stream_closed) {
streamBadge.textContent = "<%= j t("live.overlays.badge_ended") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-ended";
return;
}
const paused = !!(data.paused || data.status === "paused");
if (paused) {
streamBadge.textContent = "<%= j t("live.index.badge_paused") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
} else if (data.on_air) {
streamBadge.textContent = "<%= j t("live.index.badge_live") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-live";
} else {
streamBadge.textContent = "<%= j t("live.overlays.badge_waiting") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
}
}
let hls = null;
let playerStarted = false;
let wasOnAir = <%= @on_air ? "true" : "false" %>;
let wasPublisherOnline = <%= @publisher_online ? "true" : "false" %>;
let wasPaused = <%= @session.paused? ? "true" : "false" %>;
let reloadTimer = null;
let streamClosed = false;
let stallRecoveryAt = 0;
let firstLevelSynced = false;
let lastPlaybackAt = Date.now();
let lastPlaybackTime = 0;
const hlsUrlStable = hlsUrl;
function hlsConfig() {
return {
enableWorker: true,
lowLatencyMode: false,
liveDurationInfinity: true,
liveSyncDurationCount: 5,
liveMaxLatencyDurationCount: 14,
maxBufferLength: 30,
maxMaxBufferLength: 60,
maxBufferHole: 2.5,
maxLiveSyncPlaybackRate: 1.0,
backBufferLength: 30,
nudgeMaxRetry: 12,
xhrSetup: (xhr) => { xhr.withCredentials = true; },
};
}
function markPlaybackProgress() {
const t = video.currentTime;
if (t > lastPlaybackTime + 0.05) {
lastPlaybackTime = t;
lastPlaybackAt = Date.now();
}
}
function playbackStalledMs() {
return Date.now() - lastPlaybackAt;
}
function ensurePlaying() {
if (streamClosed || !playerStarted) return;
video.play().then(() => {
if (playHint) playHint.hidden = true;
}).catch(() => {
if (playHint) playHint.hidden = false;
});
}
/** Evita loop: startLoad su ogni waiting resetta il buffer e blocca HLS.js. */
function nudgePlayback(force) {
if (!hls || !playerStarted || streamClosed) return;
const now = Date.now();
if (!force && playbackStalledMs() < 8000) {
ensurePlaying();
return;
}
if (now - stallRecoveryAt < 5000) return;
stallRecoveryAt = now;
lastPlaybackAt = now;
hls.startLoad(-1);
ensurePlaying();
}
function showStreamEnded() {
if (streamClosed) return;
streamClosed = true;
destroyPlayer();
video.style.display = "none";
offlineMsg.hidden = true;
const panel = document.createElement("div");
panel.id = "stream-ended";
panel.className = "stream-ended";
panel.setAttribute("role", "status");
panel.innerHTML = `
<div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2><%= j t("live.show.stream_ended_title") %></h2>
<p><%= j t("live.show.stream_ended_body") %></p>
<p class="stream-ended-hint"><%= j t("live.show.stream_ended_reload_hint") %></p>
<a href="<%= public_live_index_path %>" class="btn btn-secondary stream-ended-btn"><%= j t("live.show.all_live_link") %></a>
`;
video.parentNode.insertBefore(panel, video.nextSibling);
}
function destroyPlayer() {
playerStarted = false;
if (hls) {
hls.destroy();
hls = null;
}
video.removeAttribute("src");
video.load();
}
function bindHlsEvents(instance) {
instance.on(Hls.Events.MANIFEST_PARSED, () => {
firstLevelSynced = true;
ensurePlaying();
});
instance.on(Hls.Events.LEVEL_LOADED, () => ensurePlaying());
instance.on(Hls.Events.FRAG_BUFFERED, () => markPlaybackProgress());
instance.on(Hls.Events.ERROR, (_, data) => {
if (!data.fatal) return;
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
window.setTimeout(() => {
if (!streamClosed && playerStarted) {
instance.startLoad(-1);
ensurePlaying();
}
}, 1500);
return;
}
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
instance.recoverMediaError();
return;
}
hardReloadPlayer();
});
}
function onPublisherBack() {
nudgePlayback();
}
function onResumeFromPause() {
hardReloadPlayer();
}
function onEnterPause() {
nudgePlayback();
}
function hardReloadPlayer() {
if (!Hls.isSupported()) return;
firstLevelSynced = false;
if (hls) {
hls.destroy();
hls = null;
}
const url = `${hlsUrlStable}${hlsUrlStable.includes("?") ? "&" : "?"}_t=${Date.now()}`;
hls = new Hls(hlsConfig());
hls.loadSource(url);
hls.attachMedia(video);
bindHlsEvents(hls);
hls.startLoad(-1);
playerStarted = true;
ensurePlaying();
}
/** Un solo attach HLS per sessione: MediaMTX concatena slate/camera sullo stesso path. */
function startPlayer() {
if (playerStarted) return;
if (Hls.isSupported()) {
hls = new Hls(hlsConfig());
hls.loadSource(hlsUrlStable);
hls.attachMedia(video);
bindHlsEvents(hls);
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = hlsUrlStable;
video.addEventListener("loadedmetadata", () => video.play().catch(() => {}), { once: true });
}
playerStarted = true;
}
function scheduleReload(delayMs) {
if (reloadTimer) return;
reloadTimer = setTimeout(() => {
reloadTimer = null;
pollStatus();
}, delayMs);
}
async function pollStatus() {
try {
const res = await fetch(`/live/${sessionId}/status.json?t=${Date.now()}`, { cache: "no-store" });
const data = await res.json();
const paused = !!data.paused || data.status === "paused";
const publisherOnline = !!data.publisher_online;
const awaitingSignal = !!data.awaiting_signal;
const showingCover = !!data.showing_cover;
if (data.stream_closed) {
showStreamEnded();
return;
}
syncStreamBadge(data);
const onAir = !!data.on_air;
const sessionLive = !!data.live;
if (onAir || sessionLive) {
offlineMsg.hidden = true;
if (!publisherOnline && showingCover && !paused) {
awaitingMsg.hidden = false;
awaitingMsg.textContent = "<%= j t("live.show.js_reconnecting_awaiting") %>";
} else {
awaitingMsg.hidden = !awaitingSignal;
}
pausedMsg.hidden = !paused;
if (!playerStarted) startPlayer();
if (paused && !wasPaused) onEnterPause();
if (!paused && wasPaused && playerStarted) onResumeFromPause();
if (publisherOnline && !wasPublisherOnline) onPublisherBack();
if (video.paused) ensurePlaying();
else if (playbackStalledMs() > 12000) nudgePlayback(true);
} else {
offlineMsg.hidden = false;
awaitingMsg.hidden = true;
pausedMsg.hidden = true;
}
wasOnAir = onAir;
wasPublisherOnline = publisherOnline;
wasPaused = paused;
} catch (_) {
scheduleReload(5000);
}
}
if (playHint) {
playHint.addEventListener("click", () => ensurePlaying());
}
video.addEventListener("pause", () => {
if (streamClosed || !playerStarted || video.ended || wasPaused) return;
if (playHint) playHint.hidden = false;
});
video.addEventListener("timeupdate", markPlaybackProgress);
video.addEventListener("playing", () => {
markPlaybackProgress();
if (playHint) playHint.hidden = true;
});
if (!streamClosed) {
startPlayer();
offlineMsg.hidden = wasOnAir;
window.setTimeout(() => {
if (playerStarted && !firstLevelSynced) hardReloadPlayer();
}, 12000);
} else {
offlineMsg.hidden = true;
}
pollStatus();
setInterval(pollStatus, 2000);
</script>
<% end %>
</div>