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:
103
scripts/test/adb_phone_stream_e2e.sh
Executable file
103
scripts/test/adb_phone_stream_e2e.sh
Executable 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
95
scripts/test/live_e2e_cycle.sh
Executable 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"
|
||||
40
scripts/test/rtmp_test_pattern.sh
Executable file
40
scripts/test/rtmp_test_pattern.sh
Executable 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[@]}"
|
||||
Reference in New Issue
Block a user