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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
9
infra/scripts/cron/run_rails_task.sh
Executable file
9
infra/scripts/cron/run_rails_task.sh
Executable 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 "$@"
|
||||
37
infra/scripts/garage_set_capacity.sh
Executable file
37
infra/scripts/garage_set_capacity.sh
Executable 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
|
||||
@@ -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"
|
||||
|
||||
35
infra/scripts/install_production_cron.sh
Executable file
35
infra/scripts/install_production_cron.sh
Executable 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"
|
||||
@@ -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..."
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user