- /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).
133 lines
3.6 KiB
Bash
Executable file
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
|