NextNVR/scripts/remux-faststart.sh
cclohmar 6346d5fbe1 fix: skip in-progress .part segments and enable MP4 faststart
- /api/recordings no longer lists rec_*.part.mp4 files: FFmpeg has not
  written the moov atom yet, so they can never play. The in-progress
  segment was always the newest entry at the top of the list.
- Record new segments with -movflags +faststart so the MP4 index is at
  the front. iOS/Safari otherwise tail-seeks the moov and stalls.
- Add playsinline to the playback video for inline iOS playback.
- Add scripts/remux-faststart.sh to convert the existing archive
  (dry-run by default, atomic, preserves mtime for retention).
2026-10-20 08:10:09 +01:00

133 lines
3.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# NextNVR — remux-faststart.sh
#
# Moves the MP4 index atom (moov) to the front of existing recordings so they
# start immediately in browsers, especially Safari/iOS. New recordings already
# use -movflags +faststart; this script fixes the archive recorded before that
# change.
#
# Usage:
# scripts/remux-faststart.sh [--apply] [recordings-path]
#
# Defaults to a dry run against /mnt/recordings. Writes are atomic (temp file
# in the same directory + rename) and the original mtime is preserved so the
# retention cleaner still sees the true recording age.
#
# Requirements: ffmpeg. python3 is optional — when present, files that already
# have moov before mdat are skipped.
set -euo pipefail
APPLY=0
ROOT="/mnt/recordings"
usage() {
cat <<'EOF'
Usage: remux-faststart.sh [--apply] [recordings-path]
--apply Actually rewrite files (default: dry run, report only)
-h,--help Show this help
Rewrites completed rec_*.mp4 files so their MP4 index (moov) is at the front.
In-progress *.part.mp4 files are never touched.
EOF
}
for arg in "$@"; do
case "$arg" in
--apply) APPLY=1 ;;
-h|--help) usage; exit 0 ;;
--*) echo "error: unknown option: $arg" >&2; usage >&2; exit 2 ;;
*) ROOT="$arg" ;;
esac
done
command -v ffmpeg >/dev/null 2>&1 || { echo "error: ffmpeg not found" >&2; exit 1; }
[ -d "$ROOT" ] || { echo "error: not a directory: $ROOT" >&2; exit 1; }
# Returns 0 when moov already precedes mdat (faststart), 1 when it does not,
# 2 when the file cannot be parsed.
moov_is_first() {
python3 - "$1" <<'PY'
import os
import struct
import sys
path = sys.argv[1]
size = os.path.getsize(path)
off = 0
order = []
try:
with open(path, "rb") as f:
while off < size:
f.seek(off)
hdr = f.read(8)
if len(hdr) < 8:
break
atom_size = struct.unpack(">I", hdr[:4])[0]
atom_type = hdr[4:8].decode("latin1")
if atom_size == 1:
ext = f.read(8)
if len(ext) < 8:
break
atom_size = struct.unpack(">Q", ext)[0]
order.append(atom_type)
if atom_size == 0:
break
off += atom_size
except Exception:
sys.exit(2)
if "moov" not in order or "mdat" not in order:
sys.exit(2)
sys.exit(0 if order.index("moov") < order.index("mdat") else 1)
PY
}
HAVE_PYTHON=0
command -v python3 >/dev/null 2>&1 && HAVE_PYTHON=1
total=0
converted=0
skipped=0
failed=0
echo "remux-faststart: root=$ROOT apply=$APPLY"
while IFS= read -r -d '' file; do
total=$((total + 1))
if [ "$HAVE_PYTHON" -eq 1 ] && moov_is_first "$file"; then
skipped=$((skipped + 1))
continue
fi
if [ "$APPLY" -eq 0 ]; then
echo "would remux: $file"
converted=$((converted + 1))
continue
fi
tmp="${file%.mp4}.faststart.tmp.mp4"
if ffmpeg -nostdin -v error -y -i "$file" -c copy -movflags +faststart "$tmp" \
&& touch -r "$file" "$tmp" \
&& mv -f "$tmp" "$file"; then
converted=$((converted + 1))
echo "remuxed: $file"
else
failed=$((failed + 1))
rm -f "$tmp"
echo "FAILED: $file" >&2
fi
if [ $((converted % 100)) -eq 0 ] && [ "$converted" -gt 0 ]; then
echo "--- progress: $converted processed ---"
fi
done < <(find "$ROOT" -type f -name 'rec_*.mp4' ! -name '*.part.mp4' -print0)
echo
echo "remux-faststart: scanned=$total remuxed=$converted skipped=$skipped failed=$failed"
if [ "$APPLY" -eq 0 ] && [ "$converted" -gt 0 ]; then
echo "dry run — re-run with --apply to rewrite the files above"
fi