Aggiunge overlay server-side burn-in e stabilizza diretta live.

Tabellone, badge e brand sono ricodificati nel flusso _air via OverlayRelay;
sync punteggio HTTP, volume condiviso rails/sidekiq, qualità 720p migliorata
e badge CONNECTING in ripresa da pausa.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 23:55:17 +02:00
parent fd225fbadd
commit ab9cb02083
58 changed files with 2423 additions and 242 deletions

View File

@@ -115,6 +115,7 @@ services:
condition: service_started
volumes:
- recordings:/recordings
- stream_overlays:/app/tmp/stream_overlays
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/up"]
interval: 15s
@@ -150,6 +151,7 @@ services:
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
OVERLAY_RELAY_PROCESS_CHECK: "false"
depends_on:
rails:
condition: service_healthy
@@ -157,6 +159,7 @@ services:
condition: service_started
volumes:
- recordings:/recordings
- stream_overlays:/app/tmp/stream_overlays
garage:
image: dxflrs/garage:v1.0.1
@@ -172,5 +175,6 @@ volumes:
postgres_data:
redis_data:
recordings:
stream_overlays:
garage_meta:
garage_data:

View File

@@ -22,11 +22,15 @@ rtmpAddress: :1935
hls: yes
hlsAddress: :8888
hlsVariant: mpegts
# Allineato a slate (1s) e camera; evita salti 1s→6s che glitchano HLS.js
hlsSegmentDuration: 1s
hlsSegmentMaxSize: 50M
pathDefaults:
source: publisher
overridePublisher: yes
record: yes
# Recording solo via job dedicato: il recorder inline crasha il muxer HLS al switch slate→live
record: false
recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S
recordSegmentDuration: 60s
recordDeleteAfter: 48h
@@ -37,9 +41,5 @@ paths:
# match_{uuid}:
# alwaysAvailable: true
# alwaysAvailableFile: /slates/offline.mp4
# alwaysAvailableTracks:
# - codec: H264
# - codec: MPEG4Audio
# sampleRate: 48000
# channelCount: 2
# (non usare alwaysAvailableTracks insieme al file — MediaMTX >= 2026.02)
all_others:

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Esegue un task Rails in produzione (usato da crontab).
set -euo pipefail
ROOT="${MATCHLIVETV_INFRA:-/opt/matchlivetv/infra}"
cd "$ROOT"
exec docker compose -f docker-compose.prod.yml --env-file .env exec -T rails \
bundle exec rails "$@"

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Ridimensiona capacità Garage (layout assign + apply). Uso: garage_set_capacity.sh 85G
set -euo pipefail
CAPACITY="${1:-}"
RESERVE_GB="${GARAGE_RESERVE_GB:-20}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
ENV_FILE="${ENV_FILE:-${ROOT}/.env}"
GARAGE_CONTAINER="${GARAGE_CONTAINER:-}"
if [ -z "$CAPACITY" ]; then
avail="$(df -BG "${ROOT}" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G' || echo 0)"
if [ "$avail" -lt 1 ]; then
avail="$(df -BG / 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G')"
fi
cap_gb=$((avail - RESERVE_GB))
[ "$cap_gb" -lt 10 ] && cap_gb=10
CAPACITY="${cap_gb}G"
echo "Spazio disponibile ~${avail}G, riservo ${RESERVE_GB}G → capacità Garage ${CAPACITY}"
fi
cd "$ROOT"
if [ -z "$GARAGE_CONTAINER" ]; then
GARAGE_CONTAINER="$(docker ps --format '{{.Names}}' | grep -E 'garage-1$' | head -1)"
fi
[ -n "$GARAGE_CONTAINER" ] || { echo "Container garage non trovato"; exit 1; }
node_id="$(docker exec "$GARAGE_CONTAINER" /garage status 2>/dev/null | awk '/^[0-9a-f]{16,}/{print $1; exit}')"
[ -n "$node_id" ] || { echo "Node id non trovato"; exit 1; }
echo "Node: $node_id → capacità $CAPACITY"
docker exec "$GARAGE_CONTAINER" /garage layout assign -z dc1 -c "$CAPACITY" "$node_id"
ver="$(docker exec "$GARAGE_CONTAINER" /garage layout show 2>/dev/null | awk '/layout version:/{print $NF; exit}')"
next=$((ver + 1))
docker exec "$GARAGE_CONTAINER" /garage layout apply --version "$next"
docker exec "$GARAGE_CONTAINER" /garage layout show

View File

@@ -1,13 +1,33 @@
#!/bin/sh
# Generate offline slate MP4 for MediaMTX alwaysAvailable (pausa / attesa segnale)
# Slate MP4 per MediaMTX alwaysAvailable (pausa / attesa / perdita rete).
# H.264 1280x720 30fps + AAC 48kHz mono (allineato all'app Android).
set -e
OUT_DIR="$(dirname "$0")/../slates"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
OUT_DIR="$(cd "$(dirname "$0")/../slates" && pwd)"
BRAND_IMAGE="${BRAND_IMAGE:-$ROOT/brand/CopertinaCanale_6.png}"
OUT="$OUT_DIR/offline.mp4"
if [ ! -f "$BRAND_IMAGE" ]; then
echo "Manca immagine brand: $BRAND_IMAGE" >&2
exit 1
fi
mkdir -p "$OUT_DIR"
docker run --rm -v "$OUT_DIR:/out" linuxserver/ffmpeg:latest \
-f lavfi -i color=c=0x1a1a1a:s=1280x720:r=30 \
-vf "drawtext=text='Match Live TV':fontcolor=white:fontsize=48:x=(w-text_w)/2:y=(h-text_h)/2-40,drawtext=text='Trasmissione in pausa':fontcolor=0xaaaaaa:fontsize=28:x=(w-text_w)/2:y=(h-text_h)/2+20" \
-f lavfi -i sine=frequency=440:sample_rate=48000 \
-t 30 -c:v libx264 -pix_fmt yuv420p -g 30 -preset fast \
-c:a aac -b:a 128k -y /out/offline.mp4
echo "Created $OUT"
BRAND_DIR="$(cd "$(dirname "$BRAND_IMAGE")" && pwd)"
BRAND_FILE="$(basename "$BRAND_IMAGE")"
docker run --rm \
-v "$OUT_DIR:/out" \
-v "$BRAND_DIR:/brand:ro" \
linuxserver/ffmpeg:latest \
-loop 1 -framerate 30 -i "/brand/$BRAND_FILE" \
-f lavfi -i anullsrc=r=48000:cl=mono \
-filter:v "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=0x0a0a0e" \
-map 0:v -map 1:a \
-t 30 -c:v libx264 -pix_fmt yuv420p -profile:v baseline -level 3.1 \
-x264-params "keyint=30:min-keyint=30:scenecut=0:bframes=0" \
-g 30 -keyint_min 30 -force_key_frames "expr:gte(t,n_forced*1)" \
-preset fast \
-c:a aac -b:a 128k -ac 1 -shortest -y /out/offline.mp4
echo "Created $OUT from $BRAND_IMAGE"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Installa crontab replay su server produzione (utente corrente).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
RUNNER="${ROOT}/scripts/cron/run_rails_task.sh"
LOG_DIR="${MATCHLIVETV_LOG_DIR:-/opt/matchlivetv/log}"
LOG_FILE="${LOG_DIR}/cron-replay.log"
chmod +x "$RUNNER"
mkdir -p "$LOG_DIR"
MARKER="# matchlivetv-replay-cron"
CRON_BLOCK="${MARKER}
0 3 * * * ${RUNNER} recordings:purge_expired >> ${LOG_FILE} 2>&1
0 8 * * * ${RUNNER} recordings:expiry_warnings >> ${LOG_FILE} 2>&1
"
existing="$(crontab -l 2>/dev/null || true)"
if echo "$existing" | grep -q "$MARKER"; then
echo "Crontab replay già presente, aggiorno..."
echo "$existing" | awk -v block="$CRON_BLOCK" '
BEGIN { skip=0 }
/# matchlivetv-replay-cron/ { skip=1; next }
skip && /^[^#]/ && $0 !~ /^$/ { skip=0 }
skip { next }
{ print }
END { print block }
' | crontab -
else
(echo "$existing"; echo "$CRON_BLOCK") | crontab -
fi
echo "Crontab installato:"
crontab -l | grep -A3 "$MARKER"

View File

@@ -14,6 +14,13 @@ bash "${ROOT}/scripts/ensure_garage_prod_config.sh"
echo "Avvio Garage..."
docker compose -f docker-compose.prod.yml --env-file .env up -d garage
avail_gb="$(df -BG / 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G')"
reserve="${GARAGE_RESERVE_GB:-20}"
cap_gb=$((avail_gb - reserve))
[ "$cap_gb" -lt 10 ] && cap_gb=10
export GARAGE_NODE_CAPACITY="${GARAGE_NODE_CAPACITY:-${cap_gb}G}"
echo "Capacità nodo Garage: ${GARAGE_NODE_CAPACITY} (disco ~${avail_gb}G, riserva ${reserve}G)"
bash "${ROOT}/scripts/setup_garage_replays.sh"
echo "Riavvio Rails e Sidekiq..."

View File

@@ -46,7 +46,8 @@ ensure_layout() {
exit 1
fi
echo "Node id: $node_id"
gexec layout assign -z dc1 -c 1G "$node_id" 2>/dev/null || true
local cap="${GARAGE_NODE_CAPACITY:-1G}"
gexec layout assign -z dc1 -c "$cap" "$node_id" 2>/dev/null || true
gexec layout apply --version 1 2>/dev/null || gexec layout apply 2>/dev/null || true
}

View File

@@ -1,6 +1,12 @@
# Slate video (offline)
Place `offline.mp4` here — H.264 + AAC, 1280x720, 30fps, 48kHz stereo.
Genera da `brand/CopertinaCanale_6.png`:
```bash
cd infra && bash scripts/generate_slate.sh
```
Output: `offline.mp4` — H.264 + AAC, 1280×720, 30fps, 48 kHz mono.
Must match phone encoder settings for seamless `alwaysAvailable` merge.
Generate a test slate with ffmpeg:

Binary file not shown.