374 lines
6.9 KiB
Bash
Executable file
374 lines
6.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -eu
|
|
|
|
PROJECT="/home/nvme/dockerdata/prove/s157js"
|
|
|
|
VIDEO="${1:-$PROJECT/public/photos/Fabio/original/2017Irlanda19-29ago/VID-20260920-WA0000.mp4}"
|
|
|
|
OUT="${2:-/tmp/prova-wait}"
|
|
|
|
PORT="${PORT:-9999}"
|
|
SEGMENT_TIMEOUT="${SEGMENT_TIMEOUT:-300}"
|
|
|
|
FFMPEG="/usr/local/bin/ffmpeg"
|
|
FFPROBE="/usr/local/bin/ffprobe"
|
|
|
|
WAIT_SERVER="$PROJECT/hls_cors_server_wait.py"
|
|
|
|
SERVER_PID=""
|
|
FFMPEG_PID=""
|
|
|
|
|
|
cleanup() {
|
|
echo
|
|
echo "Arresto dei processi..."
|
|
|
|
if [ -n "$FFMPEG_PID" ]; then
|
|
kill "$FFMPEG_PID" 2>/dev/null || true
|
|
fi
|
|
|
|
if [ -n "$SERVER_PID" ]; then
|
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
fi
|
|
|
|
wait 2>/dev/null || true
|
|
}
|
|
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
|
|
if [ ! -f "$VIDEO" ]; then
|
|
echo "ERRORE: video non trovato:"
|
|
echo "$VIDEO"
|
|
exit 1
|
|
fi
|
|
|
|
|
|
if [ ! -f "$WAIT_SERVER" ]; then
|
|
echo "ERRORE: server con attesa non trovato:"
|
|
echo "$WAIT_SERVER"
|
|
echo
|
|
echo "Deve esistere hls_cors_server_wait.py nel progetto."
|
|
exit 1
|
|
fi
|
|
|
|
|
|
echo "=================================================="
|
|
echo "PROVA VOD ANTICIPATA"
|
|
echo "=================================================="
|
|
echo "Video: $VIDEO"
|
|
echo "Output: $OUT"
|
|
echo "Porta: $PORT"
|
|
echo "Timeout: $SEGMENT_TIMEOUT secondi"
|
|
echo
|
|
|
|
|
|
rm -rf "$OUT"
|
|
mkdir -p "$OUT"
|
|
|
|
|
|
echo "Avvio server HTTP con attesa dei segmenti..."
|
|
|
|
python3 "$WAIT_SERVER" \
|
|
"$OUT" \
|
|
--port "$PORT" \
|
|
--segment-timeout "$SEGMENT_TIMEOUT" &
|
|
|
|
SERVER_PID=$!
|
|
|
|
|
|
sleep 0.5
|
|
|
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
echo "ERRORE: il server HTTP non è partito."
|
|
exit 1
|
|
fi
|
|
|
|
|
|
echo
|
|
echo "Avvio FFmpeg in modalità realtime..."
|
|
echo
|
|
|
|
"$FFMPEG" \
|
|
-hide_banner \
|
|
-loglevel warning \
|
|
-y \
|
|
-noautorotate \
|
|
-i "$VIDEO" \
|
|
-map 0:v:0 \
|
|
-map '0:a:0?' \
|
|
-vf "scale=1280:720:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1,format=yuv420p" \
|
|
-metadata:s:v:0 rotate=0 \
|
|
-c:v libx264 \
|
|
-profile:v high \
|
|
-level:v 4.1 \
|
|
-preset ultrafast \
|
|
-tune zerolatency \
|
|
-b:v 5000k \
|
|
-maxrate 6000k \
|
|
-bufsize 12000k \
|
|
-r 30 \
|
|
-g 90 \
|
|
-keyint_min 90 \
|
|
-sc_threshold 0 \
|
|
-c:a aac \
|
|
-b:a 160k \
|
|
-ac 2 \
|
|
-ar 48000 \
|
|
-f hls \
|
|
-hls_time 3 \
|
|
-hls_list_size 0 \
|
|
-hls_playlist_type event \
|
|
-hls_segment_type mpegts \
|
|
-hls_flags independent_segments \
|
|
-hls_segment_filename "$OUT/segment_%05d.ts" \
|
|
"$OUT/internal.m3u8" &
|
|
|
|
FFMPEG_PID=$!
|
|
|
|
|
|
echo "FFmpeg PID: $FFMPEG_PID"
|
|
echo
|
|
echo "Attendo la chiusura dei primi 3 segmenti..."
|
|
|
|
|
|
# La comparsa del segmento 3 indica che i segmenti 0, 1 e 2
|
|
# sono già stati chiusi da FFmpeg.
|
|
while [ ! -s "$OUT/segment_00003.ts" ]; do
|
|
if ! kill -0 "$FFMPEG_PID" 2>/dev/null; then
|
|
echo "ERRORE: FFmpeg è terminato prima di produrre 3 segmenti."
|
|
wait "$FFMPEG_PID" || true
|
|
exit 1
|
|
fi
|
|
|
|
COUNT="$(
|
|
find "$OUT" \
|
|
-maxdepth 1 \
|
|
-type f \
|
|
-name 'segment_*.ts' \
|
|
2>/dev/null \
|
|
| wc -l \
|
|
| tr -d ' '
|
|
)"
|
|
|
|
echo "Segmenti visibili: $COUNT"
|
|
|
|
sleep 0.2
|
|
done
|
|
|
|
|
|
echo
|
|
echo "Primi 3 segmenti completi."
|
|
echo "Creazione della playlist VOD anticipata..."
|
|
|
|
|
|
"$FFPROBE" \
|
|
-v error \
|
|
-show_entries format=duration \
|
|
-of default=noprint_wrappers=1:nokey=1 \
|
|
"$VIDEO" \
|
|
> "$OUT/.duration.txt"
|
|
|
|
|
|
python3 - "$OUT" <<'PY'
|
|
from pathlib import Path
|
|
import math
|
|
import sys
|
|
|
|
out = Path(sys.argv[1]).resolve()
|
|
|
|
duration_file = out / ".duration.txt"
|
|
playlist = out / "cast.m3u8"
|
|
temporary = out / "cast.m3u8.tmp"
|
|
|
|
duration = float(
|
|
duration_file.read_text(
|
|
encoding="utf-8"
|
|
).strip()
|
|
)
|
|
|
|
segment_duration = 3.0
|
|
|
|
total_segments = math.ceil(
|
|
duration / segment_duration
|
|
)
|
|
|
|
last_duration = (
|
|
duration -
|
|
segment_duration * (total_segments - 1)
|
|
)
|
|
|
|
if last_duration <= 0:
|
|
last_duration = segment_duration
|
|
|
|
lines = [
|
|
"#EXTM3U",
|
|
"#EXT-X-VERSION:6",
|
|
"#EXT-X-TARGETDURATION:3",
|
|
"#EXT-X-MEDIA-SEQUENCE:0",
|
|
"#EXT-X-START:TIME-OFFSET=0,PRECISE=YES",
|
|
"#EXT-X-PLAYLIST-TYPE:VOD",
|
|
"#EXT-X-INDEPENDENT-SEGMENTS",
|
|
]
|
|
|
|
for index in range(total_segments):
|
|
current_duration = (
|
|
segment_duration
|
|
if index < total_segments - 1
|
|
else last_duration
|
|
)
|
|
|
|
lines.append(
|
|
f"#EXTINF:{current_duration:.6f},"
|
|
)
|
|
|
|
lines.append(
|
|
f"segment_{index:05d}.ts"
|
|
)
|
|
|
|
lines.append("#EXT-X-ENDLIST")
|
|
|
|
temporary.write_text(
|
|
"\n".join(lines) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
temporary.replace(playlist)
|
|
|
|
real_segments = len(
|
|
list(out.glob("segment_*.ts"))
|
|
)
|
|
|
|
print()
|
|
print("Playlist VOD anticipata pubblicata")
|
|
print(f"File: {playlist}")
|
|
print(f"Durata: {duration:.6f} secondi")
|
|
print(f"Segmenti dichiarati: {total_segments}")
|
|
print(f"Segmenti già presenti: {real_segments}")
|
|
print(f"Ultimo EXTINF: {last_duration:.6f}")
|
|
PY
|
|
|
|
|
|
IP_ADDRESS="$(
|
|
hostname -I \
|
|
| awk '{print $1}'
|
|
)"
|
|
|
|
|
|
echo
|
|
echo "=================================================="
|
|
echo "PLAYLIST PRONTA"
|
|
echo "=================================================="
|
|
echo
|
|
echo "URL:"
|
|
echo "http://$IP_ADDRESS:$PORT/cast.m3u8"
|
|
echo
|
|
echo "Dal Mac esegui:"
|
|
echo
|
|
echo "catt -d 192.168.1.15 stop"
|
|
echo "catt -d 192.168.1.15 cast \\"
|
|
echo " \"http://$IP_ADDRESS:$PORT/cast.m3u8\""
|
|
echo
|
|
echo "Per provare un seek futuro:"
|
|
echo
|
|
echo "catt -d 192.168.1.15 seek 90"
|
|
echo
|
|
echo "Il server mostrerà WAIT e READY se il segmento"
|
|
echo "richiesto non è ancora disponibile."
|
|
echo "=================================================="
|
|
echo
|
|
|
|
|
|
# Attende il completamento della transcodifica.
|
|
set +e
|
|
wait "$FFMPEG_PID"
|
|
FFMPEG_STATUS=$?
|
|
set -e
|
|
|
|
FFMPEG_PID=""
|
|
|
|
|
|
if [ "$FFMPEG_STATUS" -eq 0 ]; then
|
|
echo
|
|
echo "FFmpeg ha completato la conversione."
|
|
echo "Pubblicazione della playlist reale definitiva..."
|
|
|
|
python3 - "$OUT" <<'PY'
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
out = Path(sys.argv[1]).resolve()
|
|
|
|
internal = out / "internal.m3u8"
|
|
public = out / "cast.m3u8"
|
|
temporary = out / "cast.m3u8.final.tmp"
|
|
|
|
if not internal.is_file():
|
|
raise SystemExit(
|
|
f"ERRORE: playlist interna mancante: {internal}"
|
|
)
|
|
|
|
content = internal.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
if "#EXT-X-ENDLIST" not in content:
|
|
raise SystemExit(
|
|
"ERRORE: la playlist interna non contiene ENDLIST."
|
|
)
|
|
|
|
content = content.replace(
|
|
"#EXT-X-PLAYLIST-TYPE:EVENT",
|
|
"#EXT-X-PLAYLIST-TYPE:VOD",
|
|
)
|
|
|
|
start_marker = (
|
|
"#EXT-X-START:TIME-OFFSET=0,PRECISE=YES"
|
|
)
|
|
|
|
if start_marker not in content:
|
|
lines = content.splitlines()
|
|
|
|
version_index = next(
|
|
(
|
|
index
|
|
for index, line in enumerate(lines)
|
|
if line.startswith("#EXT-X-VERSION:")
|
|
),
|
|
0,
|
|
)
|
|
|
|
lines.insert(
|
|
version_index + 1,
|
|
start_marker,
|
|
)
|
|
|
|
content = "\n".join(lines) + "\n"
|
|
|
|
elif not content.endswith("\n"):
|
|
content += "\n"
|
|
|
|
temporary.write_text(
|
|
content,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
temporary.replace(public)
|
|
|
|
print(f"Playlist finale pubblicata: {public}")
|
|
PY
|
|
|
|
else
|
|
echo
|
|
echo "ERRORE: FFmpeg è terminato con codice $FFMPEG_STATUS"
|
|
fi
|
|
|
|
|
|
echo
|
|
echo "Il server HTTP resta attivo."
|
|
echo "Premi Ctrl+C per terminare la prova."
|
|
|
|
|
|
wait "$SERVER_PID"
|