diff --git a/backend/app/controllers/public/replay_controller.rb b/backend/app/controllers/public/replay_controller.rb index 72184ab..e58dea9 100644 --- a/backend/app/controllers/public/replay_controller.rb +++ b/backend/app/controllers/public/replay_controller.rb @@ -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] diff --git a/backend/app/services/recordings/serve_from_storage.rb b/backend/app/services/recordings/serve_from_storage.rb index bccb2c5..c99c2df 100644 --- a/backend/app/services/recordings/serve_from_storage.rb +++ b/backend/app/services/recordings/serve_from_storage.rb @@ -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)) diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb index 37f4f5a..2d1eeb4 100644 --- a/backend/app/views/layouts/marketing_live.html.erb +++ b/backend/app/views/layouts/marketing_live.html.erb @@ -6,8 +6,8 @@ <%= content_for?(:title) ? yield(:title) : "Match Live TV" %> <%= render "shared/meta_tags" %> - - + + <%= yield :head %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>> diff --git a/backend/app/views/public/live/_player_overlays.html.erb b/backend/app/views/public/live/_player_overlays.html.erb index e427f10..1aab49f 100644 --- a/backend/app/views/public/live/_player_overlays.html.erb +++ b/backend/app/views/public/live/_player_overlays.html.erb @@ -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 +%>
" - ><%= @stream_closed ? "Terminata" : (@session.paused? ? "In pausa" : (@on_air ? "LIVE" : "In attesa")) %> + class="live-ovl-badge live-ovl-badge--right <%= badge_class %>" + ><%= badge_label %>
diff --git a/backend/app/views/public/replay/show.html.erb b/backend/app/views/public/replay/show.html.erb index 9c16c90..f66fc6f 100644 --- a/backend/app/views/public/replay/show.html.erb +++ b/backend/app/views/public/replay/show.html.erb @@ -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" %> -
+
<%= link_to "← Live passate", public_replay_index_path, class: "back-link" %> -
-

<%= @recording.title_or_default %>

-

- <%= @match.team.club.name %> · <%= l_local(@recording.recorded_at_or_fallback) %> + <%= live_match_page_heading(@match) %> + + <% if @recording.status == "processing" %> +

+ +

Replay in elaborazione

+

La registrazione viene preparata. Riceverai un’email quando sarà pronta.

+
+ <% elsif @recording.ready? && @recording.storage_key.present? %> +
+ + <%= render "public/live/player_overlays", + match: @match, + session: @session, + stream_closed: true, + on_air: false, + badge_label: "REPLAY", + badge_class: "badge-ended" %> +
+ +

+ <%= 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 %>

-
- <% if @recording.status == "processing" %> -
-

Registrazione in elaborazione. Riceverai un’email quando sarà pronta.

-
- <% elsif @recording.ready? && @recording.storage_key.present? %> -
- -
- -
-

- Durata <%= @recording.duration_label %> - <% if @recording.expires_at %> - · Disponibile fino al <%= l_local(@recording.expires_at) %> - <% end %> - · <%= @recording.publicly_listed? ? "Pubblico" : "Privato (link)" %> - <% if @recording.byte_size.to_i.positive? %> - · <%= @recording.byte_size_label %> - <% end %> -

- <% ent = @recording.team.entitlements %> + <% ent = @recording.team.entitlements %> + <% if (ent.phone_download_enabled? && (logged_in? || @recording.unlisted?)) || @recording.youtube_watch_url %>
<% 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 %>
-
+ <% end %> <% elsif @recording.ready? && @recording.youtube_watch_url.present? %> -
-
- -
-
-
-

- Replay disponibile su YouTube (copia server non presente). - <%= link_to "Apri su YouTube", @recording.youtube_watch_url, target: "_blank", rel: "noopener" %> -

+
+ + <%= render "public/live/player_overlays", + match: @match, + session: @session, + stream_closed: true, + on_air: false, + badge_label: "REPLAY", + badge_class: "badge-ended" %>
+

+ Replay su YouTube (copia server non presente). + <%= link_to "Apri su YouTube", @recording.youtube_watch_url, target: "_blank", rel: "noopener" %> +

<% elsif @recording.ready? %> -
-

Il file video non è più disponibile (archivio rimosso o in migrazione).

-

Durata registrata: <%= @recording.duration_label %> · <%= @recording.byte_size_label %>

+
+

File non disponibile

+

Il video non è più sul server (archivio rimosso o in migrazione).

+

Durata registrata: <%= @recording.duration_label %> · <%= @recording.byte_size_label %>

<% elsif @recording.status == "failed" %> -
-

Replay non disponibile: <%= @recording.error_message.presence || "errore di elaborazione" %>.

+
+

Replay non disponibile

+

<%= @recording.error_message.presence || "Errore di elaborazione della registrazione." %>

<% else %> -
-

Replay non più disponibile.

+
+

Replay non disponibile

+

Questa registrazione non è più accessibile.

<% end %>
diff --git a/backend/public/live.css b/backend/public/live.css index 8e84d5f..3d68bed 100644 --- a/backend/public/live.css +++ b/backend/public/live.css @@ -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 { diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml index 65c4feb..f15c20b 100644 --- a/infra/docker-compose.prod.yml +++ b/infra/docker-compose.prod.yml @@ -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: