Files
MatchLiveTv/backend/app/views/public/live/show.html.erb
Emiliano Frascaro bba6df52c0 Initial commit: monorepo Match Live TV.
Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 17:45:37 +02:00

228 lines
7.3 KiB
Plaintext

<% content_for :title do %><%= @match.team.name %> vs <%= @match.opponent_name %> — Match Live TV<% end %>
<% content_for :head do %>
<% unless @stream_closed %>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7"></script>
<% end %>
<% end %>
<div class="wrap">
<%= link_to "← Tutte le dirette", public_live_index_path, class: "back-link" %>
<h1><%= @match.team.name %> vs <%= @match.opponent_name %></h1>
<p>
<% if @stream_closed %>
<span id="status-badge" class="badge badge-ended">Diretta chiusa</span>
<% elsif @on_air %>
<span id="status-badge" class="badge badge-on-air">In onda</span>
<% else %>
<span id="status-badge" class="badge badge-wait">In attesa</span>
<% end %>
</p>
<div id="scoreboard" class="scoreboard">
<p id="sets-line" class="sets-line"><%= live_score_sets_label(@session.score_state) || "—" %></p>
<p id="partials-line" class="partials-line"<%= " hidden" unless live_score_partials_label(@session.score_state).present? %>>
Parziali: <%= live_score_partials_label(@session.score_state) %>
</p>
<p id="score-line" class="score"><%= live_score_points_label(@match, @session.score_state) %></p>
</div>
<% 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>Diretta terminata</h2>
<p>Lo streaming di questa partita è stato chiuso.</p>
<% if @session.ended_at %>
<p class="stream-ended-meta">Chiusa il <%= l(@session.ended_at.in_time_zone, format: :long) %></p>
<% end %>
<p class="stream-ended-hint">Il punteggio finale resta visibile sopra.</p>
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
</div>
<% else %>
<video id="player" controls playsinline muted autoplay></video>
<p id="offline-msg" style="display:none;color:#aaa;">La diretta non è ancora disponibile. Si aggiorna automaticamente quando torna in onda.</p>
<script>
const sessionId = "<%= @session.id %>";
const hlsUrl = "<%= j @session.hls_playback_url %>";
const video = document.getElementById("player");
const badge = document.getElementById("status-badge");
const setsLine = document.getElementById("sets-line");
const partialsLine = document.getElementById("partials-line");
const scoreLine = document.getElementById("score-line");
const offlineMsg = document.getElementById("offline-msg");
let hls = null;
let wasOnAir = <%= @on_air ? "true" : "false" %>;
let reloadTimer = null;
let stallPolls = 0;
let streamClosed = false;
function setBadge(status, live, onAir, closed) {
if (closed) {
badge.textContent = "Diretta chiusa";
badge.className = "badge badge-ended";
return;
}
if (onAir) {
badge.textContent = "In onda";
badge.className = "badge badge-on-air";
} else if (live) {
badge.textContent = "LIVE";
badge.className = "badge badge-live";
} else if (status === "reconnecting") {
badge.textContent = "Riconnessione";
badge.className = "badge badge-wait";
} else {
badge.textContent = status;
badge.className = "badge badge-wait";
}
}
function formatSets(score) {
if (!score) return "—";
return `Set ${score.current_set} · Set vinti ${score.home_sets}-${score.away_sets}`;
}
function formatPartials(score) {
const partials = score?.set_partials;
if (!partials?.length) return "";
return partials
.map((p) => `Set ${p.set} ${p.home}-${p.away}`)
.join(" · ");
}
function formatPoints(data) {
if (!data.score || !data.home_name || !data.away_name) return "—";
const s = data.score;
return `${data.home_name} ${s.home_points} - ${s.away_points} ${data.away_name}`;
}
function updateScoreboard(data) {
const score = data.score;
setsLine.textContent = formatSets(score);
const partials = formatPartials(score);
if (partials) {
partialsLine.textContent = `Parziali: ${partials}`;
partialsLine.hidden = false;
} else {
partialsLine.hidden = true;
}
scoreLine.textContent = formatPoints(data);
}
function showStreamEnded() {
if (streamClosed) return;
streamClosed = true;
destroyPlayer();
video.style.display = "none";
offlineMsg.style.display = "none";
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>Diretta terminata</h2>
<p>Lo streaming di questa partita è stato chiuso.</p>
<p class="stream-ended-hint">Il punteggio finale resta visibile sopra.</p>
<a href="<%= public_live_index_path %>" class="btn btn-secondary stream-ended-btn">Tutte le dirette</a>
`;
video.parentNode.insertBefore(panel, video.nextSibling);
}
function hlsUrlFresh() {
const sep = hlsUrl.includes("?") ? "&" : "?";
return `${hlsUrl}${sep}_ts=${Date.now()}`;
}
function destroyPlayer() {
if (hls) {
hls.destroy();
hls = null;
}
video.removeAttribute("src");
video.load();
}
function startPlayer() {
destroyPlayer();
const url = hlsUrlFresh();
if (Hls.isSupported()) {
hls = new Hls({ lowLatencyMode: true, enableWorker: true });
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
scheduleReload(2500);
}
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
video.addEventListener("loadedmetadata", () => video.play().catch(() => {}), { once: 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`);
const data = await res.json();
setBadge(data.status, data.live, data.on_air, data.stream_closed);
updateScoreboard(data);
if (data.stream_closed) {
showStreamEnded();
return;
}
const onAir = !!data.on_air;
if (onAir) {
offlineMsg.style.display = "none";
if (!wasOnAir) {
startPlayer();
stallPolls = 0;
} else if (video.readyState < 2) {
stallPolls += 1;
if (stallPolls >= 2) {
startPlayer();
stallPolls = 0;
}
} else {
stallPolls = 0;
}
} else {
offlineMsg.style.display = "block";
destroyPlayer();
stallPolls = 0;
}
wasOnAir = onAir;
} catch (_) {
scheduleReload(5000);
}
}
if (wasOnAir) {
startPlayer();
} else {
offlineMsg.style.display = "block";
}
pollStatus();
setInterval(pollStatus, 3000);
</script>
<% end %>
</div>