Allinea replay alla UI live e corregge miniature e loghi.
Player contenuto come la diretta, fix streaming thumbnail da S3, volume persistente ActiveStorage in produzione. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
module Public
|
||||
class ReplayController < SiteBaseController
|
||||
include Public::LiveHelper
|
||||
|
||||
layout "marketing_live"
|
||||
|
||||
before_action :set_session_and_recording, only: %i[show stream thumbnail download]
|
||||
|
||||
@@ -6,6 +6,8 @@ module Recordings
|
||||
# Serve file replay/thumbnail al browser senza esporre URL S3 interni (es. http://garage:3900).
|
||||
class ServeFromStorage
|
||||
CHUNK_SIZE = 256 * 1024
|
||||
# Thumbnail e file piccoli: buffer completo (l'Enumerator su Puma può non completare la risposta).
|
||||
SMALL_FILE_MAX = 2 * 1024 * 1024
|
||||
|
||||
def initialize(key:, content_type:, disposition: "inline", filename: nil)
|
||||
@key = key
|
||||
@@ -55,6 +57,15 @@ module Recordings
|
||||
|
||||
controller.status = :partial_content if range.present? && resp.content_range.present?
|
||||
|
||||
if range.blank? && resp.content_length.to_i.positive? && resp.content_length.to_i <= SMALL_FILE_MAX
|
||||
return controller.send_data(
|
||||
resp.body.read,
|
||||
type: @content_type,
|
||||
disposition: @disposition,
|
||||
filename: @filename
|
||||
)
|
||||
end
|
||||
|
||||
body = resp.body
|
||||
controller.response_body = Enumerator.new do |yielder|
|
||||
while (chunk = body.read(CHUNK_SIZE))
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||
<%= render "shared/meta_tags" %>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<link rel="stylesheet" href="/marketing.css?v=36">
|
||||
<link rel="stylesheet" href="/live.css?v=24">
|
||||
<link rel="stylesheet" href="/marketing.css?v=38">
|
||||
<link rel="stylesheet" href="/live.css?v=26">
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
<body<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<%
|
||||
closed = local_assigns.fetch(:stream_closed) { @stream_closed }
|
||||
on_air = local_assigns.fetch(:on_air) { @on_air }
|
||||
badge_label = local_assigns[:badge_label]
|
||||
badge_class = local_assigns[:badge_class]
|
||||
|
||||
unless badge_label
|
||||
badge_label = closed ? "Terminata" : (session.paused? ? "In pausa" : (on_air ? "LIVE" : "In attesa"))
|
||||
end
|
||||
|
||||
unless badge_class
|
||||
badge_class = closed ? "badge-ended" : (session.paused? ? "badge-wait" : (on_air ? "badge-live" : "badge-wait"))
|
||||
end
|
||||
%>
|
||||
<div class="live-player-overlays" id="live-overlays">
|
||||
<span
|
||||
id="live-stream-badge"
|
||||
class="live-ovl-badge live-ovl-badge--right <%= @stream_closed ? "badge-ended" : (@session.paused? ? "badge-wait" : (@on_air ? "badge-live" : "badge-wait")) %>"
|
||||
><%= @stream_closed ? "Terminata" : (@session.paused? ? "In pausa" : (@on_air ? "LIVE" : "In attesa")) %></span>
|
||||
class="live-ovl-badge live-ovl-badge--right <%= badge_class %>"
|
||||
><%= badge_label %></span>
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,49 @@
|
||||
<% club_name = @match.team.club&.name %>
|
||||
<% content_for :title, "Replay — #{@recording.title_or_default}" %>
|
||||
<% content_for :meta_description, "Replay #{@recording.title_or_default} su Match Live TV." %>
|
||||
<% content_for :robots, @recording.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
|
||||
|
||||
<div class="wrap replay-show">
|
||||
<div class="wrap">
|
||||
<%= link_to "← Live passate", public_replay_index_path, class: "back-link" %>
|
||||
|
||||
<header class="replay-show__header">
|
||||
<h1><%= @recording.title_or_default %></h1>
|
||||
<p class="replay-show__meta">
|
||||
<%= @match.team.club.name %> · <%= l_local(@recording.recorded_at_or_fallback) %>
|
||||
<%= live_match_page_heading(@match) %>
|
||||
|
||||
<% if @recording.status == "processing" %>
|
||||
<div class="stream-ended" role="status">
|
||||
<div class="stream-ended-icon" aria-hidden="true">⏳</div>
|
||||
<h2>Replay in elaborazione</h2>
|
||||
<p>La registrazione viene preparata. Riceverai un’email quando sarà pronta.</p>
|
||||
</div>
|
||||
<% elsif @recording.ready? && @recording.storage_key.present? %>
|
||||
<div class="live-player-wrap">
|
||||
<video id="replay-player" controls playsinline preload="metadata"
|
||||
poster="<%= @recording.thumbnail_url.presence || '/images/copertina-canale.png' %>">
|
||||
<source src="<%= public_replay_stream_path(@session) %>" type="video/mp4">
|
||||
</video>
|
||||
<%= render "public/live/player_overlays",
|
||||
match: @match,
|
||||
session: @session,
|
||||
stream_closed: true,
|
||||
on_air: false,
|
||||
badge_label: "REPLAY",
|
||||
badge_class: "badge-ended" %>
|
||||
</div>
|
||||
|
||||
<p class="replay-show__meta-line">
|
||||
<%= l_local(@recording.recorded_at_or_fallback) %>
|
||||
· Durata <%= @recording.duration_label %>
|
||||
· <%= @recording.views_label %>
|
||||
<% if @recording.expires_at %>
|
||||
· Fino al <%= l_local(@recording.expires_at) %>
|
||||
<% end %>
|
||||
· <%= @recording.publicly_listed? ? "Pubblico" : "Privato (link)" %>
|
||||
<% if @recording.source_platform_label != "—" %>
|
||||
· <%= @recording.source_platform_label %>
|
||||
<% end %>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<% if @recording.status == "processing" %>
|
||||
<div class="card replay-show__status">
|
||||
<p>Registrazione in elaborazione. Riceverai un’email quando sarà pronta.</p>
|
||||
</div>
|
||||
<% elsif @recording.ready? && @recording.storage_key.present? %>
|
||||
<div class="replay-show__player card">
|
||||
<video id="replay-player" class="replay-video" controls playsinline preload="metadata"
|
||||
<% if @recording.thumbnail_url %> poster="<%= @recording.thumbnail_url %>"<% end %>>
|
||||
<source src="<%= public_replay_stream_path(@session) %>" type="video/mp4">
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div class="replay-show__details card">
|
||||
<p class="replay-show__details-line">
|
||||
<span>Durata <%= @recording.duration_label %></span>
|
||||
<% if @recording.expires_at %>
|
||||
<span>· Disponibile fino al <%= l_local(@recording.expires_at) %></span>
|
||||
<% end %>
|
||||
<span>· <%= @recording.publicly_listed? ? "Pubblico" : "Privato (link)" %></span>
|
||||
<% if @recording.byte_size.to_i.positive? %>
|
||||
<span>· <%= @recording.byte_size_label %></span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% ent = @recording.team.entitlements %>
|
||||
<% if (ent.phone_download_enabled? && (logged_in? || @recording.unlisted?)) || @recording.youtube_watch_url %>
|
||||
<div class="replay-show__actions">
|
||||
<% if ent.phone_download_enabled? && (logged_in? || @recording.unlisted?) %>
|
||||
<%= link_to "Scarica MP4", public_replay_download_path(@session), class: "btn btn-primary" %>
|
||||
@@ -48,35 +52,41 @@
|
||||
<%= link_to "Apri su YouTube", @recording.youtube_watch_url, class: "btn btn-secondary", target: "_blank", rel: "noopener" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% elsif @recording.ready? && @recording.youtube_watch_url.present? %>
|
||||
<div class="replay-show__player card">
|
||||
<div class="replay-video" style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;background:#000;border-radius:12px">
|
||||
<div class="live-player-wrap live-player-wrap--embed">
|
||||
<iframe src="https://www.youtube.com/embed/<%= @recording.youtube_video_id %>"
|
||||
title="<%= @recording.title_or_default %>"
|
||||
style="position:absolute;top:0;left:0;width:100%;height:100%;border:0"
|
||||
class="live-player-embed"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
<%= render "public/live/player_overlays",
|
||||
match: @match,
|
||||
session: @session,
|
||||
stream_closed: true,
|
||||
on_air: false,
|
||||
badge_label: "REPLAY",
|
||||
badge_class: "badge-ended" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="replay-show__details card">
|
||||
<p class="replay-show__details-line">
|
||||
Replay disponibile su YouTube (copia server non presente).
|
||||
<p class="replay-show__meta-line">
|
||||
Replay su YouTube (copia server non presente).
|
||||
<%= link_to "Apri su YouTube", @recording.youtube_watch_url, target: "_blank", rel: "noopener" %>
|
||||
</p>
|
||||
</div>
|
||||
<% elsif @recording.ready? %>
|
||||
<div class="card replay-show__status">
|
||||
<p>Il file video non è più disponibile (archivio rimosso o in migrazione).</p>
|
||||
<p class="replay-show__details-line">Durata registrata: <%= @recording.duration_label %> · <%= @recording.byte_size_label %></p>
|
||||
<div class="stream-ended" role="status">
|
||||
<h2>File non disponibile</h2>
|
||||
<p>Il video non è più sul server (archivio rimosso o in migrazione).</p>
|
||||
<p class="stream-ended-meta">Durata registrata: <%= @recording.duration_label %> · <%= @recording.byte_size_label %></p>
|
||||
</div>
|
||||
<% elsif @recording.status == "failed" %>
|
||||
<div class="card replay-show__status">
|
||||
<p>Replay non disponibile: <%= @recording.error_message.presence || "errore di elaborazione" %>.</p>
|
||||
<div class="stream-ended" role="status">
|
||||
<h2>Replay non disponibile</h2>
|
||||
<p><%= @recording.error_message.presence || "Errore di elaborazione della registrazione." %></p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="card replay-show__status">
|
||||
<p>Replay non più disponibile.</p>
|
||||
<div class="stream-ended" role="status">
|
||||
<h2>Replay non disponibile</h2>
|
||||
<p>Questa registrazione non è più accessibile.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,46 @@
|
||||
/* Stili pagine dirette — usa layout marketing + marketing.css */
|
||||
.live-main {
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.live-main .wrap {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 20px 48px;
|
||||
}
|
||||
|
||||
/* Player contenuto: stesso aspetto su diretta e replay (non full-screen). */
|
||||
.live-main .live-player-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
width: min(100%, 900px, calc(50vh * 16 / 9));
|
||||
margin: 0 auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: min(70vh, calc(100vw * 9 / 16));
|
||||
min-height: 200px;
|
||||
min-height: 180px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
contain: layout style;
|
||||
contain: layout style paint;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.live-main video {
|
||||
.live-main .live-player-wrap video,
|
||||
.live-main .live-player-wrap .live-player-embed {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.live-main .live-player-wrap--embed {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* Sovraimpressioni chiare sul video (copertina scura). */
|
||||
@@ -585,48 +599,22 @@
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.live-main .replay-show__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.live-main .replay-show__header h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.live-main .replay-show__meta {
|
||||
margin: 0;
|
||||
color: #999;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.live-main .replay-show__player {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.live-main .replay-show__player .replay-video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.live-main .replay-show__details {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.live-main .replay-show__details-line {
|
||||
margin: 0 0 14px;
|
||||
.live-main .replay-show__meta-line {
|
||||
margin: 14px 0 0;
|
||||
color: #aaa;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.live-main .replay-show__meta-line a {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.live-main .replay-show__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.live-main .replay-filter-form {
|
||||
|
||||
@@ -115,6 +115,7 @@ services:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- recordings:/recordings
|
||||
- rails_storage:/rails/storage
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/up"]
|
||||
interval: 15s
|
||||
@@ -169,6 +170,7 @@ services:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- recordings:/recordings
|
||||
- rails_storage:/rails/storage
|
||||
|
||||
garage:
|
||||
image: dxflrs/garage:v1.0.1
|
||||
@@ -184,5 +186,6 @@ volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
recordings:
|
||||
rails_storage:
|
||||
garage_meta:
|
||||
garage_data:
|
||||
|
||||
Reference in New Issue
Block a user