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

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# E2E streaming: installa APK, login demo, apre camera sessione attiva.
# Richiede: USB debug + su MIUI/Xiaomi abilitare "Installazione via USB".
set -euo pipefail
export PATH="$HOME/flutter/bin:$HOME/Android/Sdk/platform-tools:$PATH"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
DEVICE="${ANDROID_DEVICE:-$(adb devices | awk '/device$/{print $1; exit}')}"
SESSION_ID="${SESSION_ID:-fe09a29f-79db-44d5-864f-397942c0a2c5}"
EMAIL="${TEST_EMAIL:-coach@matchlivetv.test}"
PASSWORD="${TEST_PASSWORD:-password123}"
APK="$ROOT/mobile/build/app/outputs/flutter-apk/app-release.apk"
PKG="com.matchlivetv.match_live_tv"
if [[ -z "$DEVICE" ]]; then
echo "Nessun telefono USB. Collega il device e abilita debug USB."
exit 1
fi
echo "== Device: $DEVICE =="
if [[ ! -f "$APK" ]]; then
echo "Build APK release..."
(cd "$ROOT/mobile" && flutter build apk --release --dart-define=API_BASE_URL=https://www.matchlivetv.it)
fi
echo "== Install APK (conferma sul telefono se richiesto) =="
if ! adb -s "$DEVICE" install -r -g "$APK"; then
echo ""
echo "Installazione bloccata. Su Xiaomi/MIUI:"
echo " Impostazioni → Impostazioni aggiuntive → Opzioni sviluppatore → Installazione via USB → Attiva"
echo "Poi rilancia questo script."
exit 1
fi
echo "== Grant permissions =="
adb -s "$DEVICE" shell pm grant "$PKG" android.permission.CAMERA 2>/dev/null || true
adb -s "$DEVICE" shell pm grant "$PKG" android.permission.RECORD_AUDIO 2>/dev/null || true
adb -s "$DEVICE" shell pm grant "$PKG" android.permission.POST_NOTIFICATIONS 2>/dev/null || true
echo "== Launch app =="
adb -s "$DEVICE" shell am force-stop "$PKG"
adb -s "$DEVICE" shell am start -n "$PKG/.MainActivity"
sleep 4
tap_text() {
local text="$1"
adb -s "$DEVICE" shell uiautomator dump /sdcard/ui.xml >/dev/null 2>&1 || true
local coords
coords=$(adb -s "$DEVICE" shell "grep -o \"text=\\\"$text\\\"[^>]*bounds=\\\"\\[[0-9,]*\\]\\[[0-9,]*\\]\\\"\" /sdcard/ui.xml 2>/dev/null | head -1" || true)
if [[ -z "$coords" ]]; then return 1; fi
local b
b=$(echo "$coords" | sed -n 's/.*bounds=\"\[\([div0-9,]*\)\]\[\([0-9,]*\)\]\".*/\1 \2/p')
local x1 y1 x2 y2
IFS=',' read -r x1 y1 <<< "${b%% *}"
IFS=',' read -r x2 y2 <<< "${b##* }"
local cx=$(( (x1 + x2) / 2 ))
local cy=$(( (y1 + y2) / 2 ))
adb -s "$DEVICE" shell input tap "$cx" "$cy"
return 0
}
tap_contains() {
local needle="$1"
adb -s "$DEVICE" shell uiautomator dump /sdcard/ui.xml >/dev/null 2>&1 || true
local line
line=$(adb -s "$DEVICE" shell "grep -i '$needle' /sdcard/ui.xml | head -1" || true)
[[ -n "$line" ]] || return 1
local bounds
bounds=$(echo "$line" | sed -n 's/.*bounds=\"\[\([0-9,]*\)\]\[\([0-9,]*\)\]\".*/\1 \2/p')
local x1 y1 x2 y2
IFS=',' read -r x1 y1 <<< "${bounds%% *}"
IFS=',' read -r x2 y2 <<< "${bounds##* }"
adb -s "$DEVICE" shell input tap $(( (x1 + x2) / 2 )) $(( (y1 + y2) / 2 ))
}
echo "== Login (demo) =="
# Email field
adb -s "$DEVICE" shell uiautomator dump /sdcard/ui.xml >/dev/null 2>&1 || true
sleep 1
# Tap center-left for email field (Flutter fallback coords 720p)
adb -s "$DEVICE" shell input tap 540 520
adb -s "$DEVICE" shell input keyevent KEYCODE_MOVE_END
adb -s "$DEVICE" shell input keyevent --longpress $(printf '%s' "$EMAIL" | od -An -tuC | tr -s ' ' '\n' | while read -r c; do echo "KEYCODE_DEL"; done | head -40) 2>/dev/null || true
adb -s "$DEVICE" shell input text "${EMAIL//@/%40}"
sleep 1
tap_contains "Accedi" || tap_contains "Entra" || adb -s "$DEVICE" shell input tap 540 900
sleep 5
echo "== Open camera for session $SESSION_ID =="
# Deep link via am start with route — apri wizard/camera se già loggato
adb -s "$DEVICE" shell am start -a android.intent.action.VIEW \
-d "https://www.matchlivetv.it/live/$SESSION_ID" "$PKG" 2>/dev/null || true
sleep 3
tap_contains "Riprendi" || tap_contains "RIPRENDI" || tap_contains "camera" || true
sleep 2
echo "== Session status =="
curl -sS "https://www.matchlivetv.it/live/$SESSION_ID/status.json" | python3 -m json.tool | head -20
echo ""
echo "Apri nel browser: https://www.matchlivetv.it/live/$SESSION_ID"
echo "Controlla bitrate/fps dispositivo via API sessions."

95
scripts/test/live_e2e_cycle.sh Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# E2E ciclo diretta web: status, HLS, pausa, ripresa.
set -euo pipefail
SESSION_ID="${SESSION_ID:-fe09a29f-79db-44d5-864f-397942c0a2c5}"
API="${API:-https://www.matchlivetv.it}"
EMAIL="${TEST_EMAIL:-coach@matchlivetv.test}"
PASSWORD="${TEST_PASSWORD:-password123}"
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo "OK: $*"; }
echo "== Login =="
TOKEN=$(curl -sf -X POST "$API/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$EMAIL\",\"password\":\"$PASSWORD\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
AUTH="Authorization: Bearer $TOKEN"
echo "== Status iniziale =="
INIT=$(curl -sf "$API/live/$SESSION_ID/status.json")
echo "$INIT" | python3 -m json.tool
python3 -c "
import json,sys
d=json.loads('''$INIT''')
assert not d.get('stream_closed'), d
print('status', d.get('status'), 'on_air', d.get('on_air'), 'publisher', d.get('publisher_online'))
"
echo "== HLS master =="
MASTER=$(curl -sf "$API/hls/live/match_${SESSION_ID}/index.m3u8")
echo "$MASTER" | head -8
echo "$MASTER" | grep -q main_stream || fail "master senza variant"
VARIANT_LINE=$(echo "$MASTER" | grep main_stream | head -1 | tr -d '\r')
VARIANT_URL="$API/hls/live/match_${SESSION_ID}/${VARIANT_LINE}"
MEDIA=$(curl -sf "$VARIANT_URL")
SEG=$(echo "$MEDIA" | grep '_main_seg' | tail -1 | tr -d '\r')
[[ -n "$SEG" ]] || fail "nessun segmento in playlist"
SEG_URL="$API/hls/live/match_${SESSION_ID}/${SEG}"
SZ=$(curl -sf -o /tmp/live_e2e_seg.ts -w '%{size_download}' "$SEG_URL")
[[ "${SZ:-0}" -gt 1000 ]] || fail "segmento troppo piccolo ($SZ bytes)"
ok "HLS segmento ${SZ} bytes"
if [[ "$(echo "$INIT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('paused'))")" == "True" ]]; then
echo "== Sessione in pausa: test resume =="
curl -sf -X PATCH -H "$AUTH" "$API/api/v1/sessions/$SESSION_ID/resume" >/dev/null
sleep 6
fi
echo "== Verifica live (publisher atteso) =="
for i in 1 2 3 4 5 6 7 8 9 10; do
ST=$(curl -sf "$API/live/$SESSION_ID/status.json")
PUB=$(echo "$ST" | python3 -c "import sys,json; print(json.load(sys.stdin).get('publisher_online'))")
PAUSED=$(echo "$ST" | python3 -c "import sys,json; print(json.load(sys.stdin).get('paused'))")
echo " poll $i paused=$PAUSED publisher_online=$PUB"
if [[ "$PAUSED" == "False" && "$PUB" == "True" ]]; then
ok "live attivo"
break
fi
sleep 2
if [[ "$i" == "10" ]]; then
echo "WARN: live non confermato entro 20s (continuo pausa test)"
fi
done
echo "== Pausa API =="
curl -sf -X PATCH -H "$AUTH" "$API/api/v1/sessions/$SESSION_ID/pause" >/dev/null
sleep 8
PAUSE_ST=$(curl -sf "$API/live/$SESSION_ID/status.json")
echo "$PAUSE_ST" | python3 -m json.tool | head -12
python3 -c "
import json,sys
d=json.loads('''$PAUSE_ST''')
assert d.get('paused') or d.get('status')=='paused', d
print('paused OK')
"
echo "== Ripresa API =="
curl -sf -X PATCH -H "$AUTH" "$API/api/v1/sessions/$SESSION_ID/resume" >/dev/null
sleep 8
RES=$(curl -sf "$API/live/$SESSION_ID/status.json")
echo "$RES" | python3 -m json.tool | head -12
python3 -c "
import json,sys
d=json.loads('''$RES''')
assert not d.get('paused'), d
assert d.get('status') in ('live','connecting'), d
print('resume OK')
"
echo "== Telemetry device =="
curl -sf -H "$AUTH" "$API/api/v1/sessions/$SESSION_ID" \
| python3 -c "import sys,json; d=json.load(sys.stdin); dev=d.get('devices') or []; print('devices', dev[:2])"
ok "Ciclo E2E API/HLS completato"

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Pubblica un pattern colorato su RTMP per testare il player web (senza telefono).
# Uso:
# SESSION_ID=fe09a29f-79db-44d5-864f-397942c0a2c5 ./scripts/test/rtmp_test_pattern.sh
# RTMP_URL=rtmp://www.matchlivetv.it:1935/live/match_UUID ./scripts/test/rtmp_test_pattern.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
OUT_DIR="${TMPDIR:-/tmp}/matchlivetv-test"
PATTERN="$OUT_DIR/test_pattern.mp4"
DURATION="${DURATION_SEC:-0}" # 0 = loop infinito
if [[ -z "${RTMP_URL:-}" ]]; then
if [[ -z "${SESSION_ID:-}" ]]; then
echo "Imposta SESSION_ID o RTMP_URL"
exit 1
fi
RTMP_URL="rtmp://www.matchlivetv.it:1935/live/match_${SESSION_ID}"
fi
mkdir -p "$OUT_DIR"
if [[ ! -f "$PATTERN" ]]; then
echo "Genero video di test $PATTERN ..."
ffmpeg -y -hide_banner -loglevel warning \
-f lavfi -i "testsrc2=size=1280x720:rate=30" \
-f lavfi -i "sine=frequency=880:sample_rate=48000" \
-t 30 \
-c:v libx264 -pix_fmt yuv420p -profile:v baseline -level 3.1 -g 30 -keyint_min 30 \
-c:a aac -b:a 128k -ac 1 \
"$PATTERN"
fi
echo "Pubblico su $RTMP_URL (Ctrl+C per fermare)"
ARGS=(-re -stream_loop -1 -i "$PATTERN" -c copy -f flv "$RTMP_URL")
if [[ "$DURATION" != "0" ]]; then
ARGS=(-re -t "$DURATION" -i "$PATTERN" -c copy -f flv "$RTMP_URL")
fi
exec ffmpeg -hide_banner "${ARGS[@]}"