bunch of shit
This commit is contained in:
@@ -16,6 +16,12 @@
|
||||
# alone (zero copies); a non-compliant file is converted to name.mp4 (name.h264.mp4 if the raw is
|
||||
# already .mp4), the raw's name is appended to .stlocal (an #include'd ignore list) so Syncthing
|
||||
# drops it without flagging or re-pulling, then the raw is deleted. One copy on the server.
|
||||
# - reconnect fragments of ONE stream (same user, same title, fewer than STITCH_MAX_PARTS (500) files, gaps under
|
||||
# STITCH_MAX_GAP_S) are first stitched back into one file by vgstitch.py (lossless concat; the arrival-time
|
||||
# file names say where every piece sits), then published as one recording. Joins are recorded in the sidecar.
|
||||
# Nothing in a stream's .live/ is touched until the stream is over (no open or recently written file).
|
||||
# - a LONE stream recording under MIN_LIVE_SECONDS (10) is a reconnect stub with nothing to join to: moved to
|
||||
# trash, never published. Only .live/ files — clips are frequently a few seconds long and are never subject to this.
|
||||
# - failure: name.normalize-failed.log is written and the file is skipped until that log is deleted
|
||||
# Idempotent; safe every few minutes.
|
||||
set -uo pipefail
|
||||
@@ -32,6 +38,18 @@ KEEP_RAW_STREAMS="${KEEP_RAW_STREAMS:-0}"
|
||||
# Encoder threads for every re-encode (clips and streams). 2 leaves the box responsive for Jellyfin/live viewers;
|
||||
# conversions take longer. systemd Environment=ENCODE_THREADS=... to change.
|
||||
ENCODE_THREADS="${ENCODE_THREADS:-2}"
|
||||
# STREAM recordings shorter than this are reconnect stubs, not recordings: every time a publisher drops and
|
||||
# comes back, MediaMTX opens a new file, and one bad night left 471 one- and two-second files in .live/.
|
||||
# They go to trash (3-day undo) instead of being published. Applies ONLY to .live/ — clips are often a few
|
||||
# seconds long on purpose and are never touched by this. 0 disables. systemd Environment=MIN_LIVE_SECONDS=…
|
||||
MIN_LIVE_SECONDS="${MIN_LIVE_SECONDS:-10}"
|
||||
# Stitching (vgstitch.py): fewer than this many fragments in one stream's .live/ are joined into one recording.
|
||||
# 500, not 20: on a lossy uplink MediaMTX restarts the recorder every ~10 s (a keyframe whose slices were lost
|
||||
# leaves an access unit with only SPS/PPS, which the recorder refuses), so a half-hour stream is ~180 same-
|
||||
# parameter files two seconds apart — exactly the case for stitching. The gap rule (STITCH_MAX_GAP_S seconds
|
||||
# between fragments = separate recordings) is the real guard. 0 parts disables stitching. systemd Environment=…
|
||||
STITCH_MAX_PARTS="${STITCH_MAX_PARTS:-500}"
|
||||
STITCH_MAX_GAP_S="${STITCH_MAX_GAP_S:-600}"
|
||||
MAX_W=2560; MAX_H=1440; MAX_FPS=60
|
||||
exec 9>"${LOCK_FILE:-/run/lock/normalize-sweep.lock}"
|
||||
flock -n 9 || exit 0
|
||||
@@ -50,6 +68,11 @@ vglog() { local lvl="$1"; shift; printf 'ts=%s level=%s component=sweep %s\n' "$
|
||||
log() { vglog info "msg=\"$*\""; }
|
||||
dur() { ffprobe -v error -show_entries format=duration -of csv=p=0 "$1" 2>/dev/null | cut -d. -f1; }
|
||||
|
||||
# A stream's fragments are one recording: join them before anything below looks at them one by one.
|
||||
if (( STITCH_MAX_PARTS > 0 )) && [[ -r "$(dirname "$0")/vgstitch.py" ]]; then # installed side by side
|
||||
python3 "$(dirname "$0")/vgstitch.py" "$ROOT" --max-parts "$STITCH_MAX_PARTS" --max-gap-s "$STITCH_MAX_GAP_S" --idle-min "$MINS_IDLE"
|
||||
fi
|
||||
|
||||
# --- probe: prints "verdict reason" where verdict ∈ ok|remux|encode|skip ----------------------
|
||||
probe() {
|
||||
local f="$1" ext="${1##*.}"
|
||||
@@ -123,6 +146,31 @@ while IFS= read -r -d '' src; do
|
||||
fi
|
||||
fuser -s "$src" 2>/dev/null && continue # still being written
|
||||
[[ -e "$src" ]] || continue # dumped meanwhile
|
||||
if (( live )); then
|
||||
# The stream is still going while any sibling fragment is open or fresh: publishing this piece now would
|
||||
# make it a separate recording that the stitcher can never join. Wait for the stream to end.
|
||||
going=0
|
||||
for sib in "$(dirname "$src")"/*.mp4; do
|
||||
[[ -e $sib && $sib != "$src" ]] || continue
|
||||
if fuser -s "$sib" 2>/dev/null || [[ -n $(find "$sib" -mmin -"$MINS_IDLE" 2>/dev/null) ]]; then going=1; break; fi
|
||||
done
|
||||
if (( going )); then vglog info "event=defer reason=\"stream still going\" file=\"$src\" stream=\"$stream\""; continue; fi
|
||||
fi
|
||||
|
||||
if (( live && MIN_LIVE_SECONDS > 0 )); then
|
||||
secs=$(dur "$src"); secs=${secs:-0}
|
||||
if (( secs < MIN_LIVE_SECONDS )); then
|
||||
night="$(date +%F)"; rel="${src#"$ROOT"/}"; dst="$ROOT/trash/$night/$rel"
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
mv -f "$src" "$dst"
|
||||
[[ -e "${src%.*}.json" ]] && mv -f "${src%.*}.json" "$(dirname "$dst")/"
|
||||
rm -f "${src%.*}.stitch.json"
|
||||
rmdir "$(dirname "$src")" "$titledir" 2>/dev/null || true
|
||||
vgmet_add "jobs:discard:done:stream"
|
||||
vglog info "event=discarded reason=too_short seconds=$secs min=$MIN_LIVE_SECONDS file=\"$src\" stream=\"$stream\" trash=\"$dst\""
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
read -r verdict reason <<<"$(probe "$src")"
|
||||
# Keep what the PROBE said: the deband branch below overwrites $reason with its own text, which silently
|
||||
@@ -220,8 +268,19 @@ PYEOF
|
||||
(( deband )) && vgmet_add deband 1
|
||||
if (( live )); then
|
||||
mv -f "$tmp" "$out"; chown 1000:1000 "$out" 2>/dev/null
|
||||
if [[ -n $tcjson ]]; then # sidecar: exact capture time (read by the Jellyfin plugin) + drift samples
|
||||
printf '%s\n' "$tcjson" > "${out%.*}.json"; chown 1000:1000 "${out%.*}.json" 2>/dev/null
|
||||
stitchjson="${src%.*}.stitch.json" # vgstitch.py: parts, offsets and the time missing at each join
|
||||
if [[ -n $tcjson || -e $stitchjson ]]; then # sidecar: exact capture time (read by the Jellyfin plugin) + drift samples
|
||||
python3 - "$tcjson" "$stitchjson" > "${out%.*}.json" <<'PYEOF'
|
||||
import json, sys, os
|
||||
tc, sj = sys.argv[1], sys.argv[2]
|
||||
d = json.loads(tc) if tc else {}
|
||||
if os.path.exists(sj):
|
||||
d["stitched"] = json.load(open(sj))
|
||||
# the strip decode's drift figure spans the joins too, so it is not clock drift on a stitched file
|
||||
if "drift_ms" in d: d["drift_ms_note"] = "includes collapsed joins; see stitched.joins"
|
||||
print(json.dumps(d))
|
||||
PYEOF
|
||||
chown 1000:1000 "${out%.*}.json" 2>/dev/null; rm -f "$stitchjson"
|
||||
fi
|
||||
if [[ $verdict == encode ]] && (( ! deband || KEEP_RAW_STREAMS )); then # gap-filled re-encode (or asked to): keep the raw in the hidden .src/
|
||||
srcdir="$(dirname "$out")/.src"; mkdir -p "$srcdir"; chown 1000:1000 "$srcdir" 2>/dev/null
|
||||
|
||||
Reference in New Issue
Block a user