Distinguishes a genuine audio mismatch at a clip boundary (evidence the inputs aren't really a continuous sequence) from a boundary that's simply too quiet to judge either way, using an RMS floor alongside the existing cross-correlation confidence score. A confident mismatch now aborts before encoding instead of silently splicing unrelated clips together; --allow-discontinuous opts back into the old behavior.
101 lines
4.5 KiB
Markdown
101 lines
4.5 KiB
Markdown
# live-photo-concat
|
|
|
|
Concatenate sequential iPhone Live Photo `.MOV` clips into a single seamless,
|
|
metadata-preserving video — with the real duplicate footage between clips
|
|
removed.
|
|
|
|
## Why
|
|
|
|
Each iPhone Live Photo `.MOV` captures roughly 1.5 seconds before and after
|
|
its key moment. When several Live Photos are taken in quick succession (e.g.
|
|
rapid-fire shutter presses), consecutive clips genuinely overlap: the same
|
|
seconds of real-world video and audio get captured twice, once at the end of
|
|
clip *N* and again at the start of clip *N+1*. Naively concatenating the
|
|
clips repeats that footage, which shows up as an odd "loop" at each clip
|
|
boundary.
|
|
|
|
This script:
|
|
|
|
1. Extracts the real video+audio streams from each clip (iPhone Live Photo
|
|
files bundle a handful of extra HDR/depth/metadata tracks that aren't
|
|
needed here).
|
|
2. Cross-correlates the audio at each clip boundary to measure the *actual*
|
|
overlap duration from the content itself (not just filename order or
|
|
whole-second creation timestamps, which aren't precise enough). If a
|
|
boundary's audio simply doesn't match — a sign the inputs aren't actually
|
|
a continuous sequence (wrong order, a missing clip, unrelated files) — the
|
|
run aborts before encoding anything, rather than silently splicing
|
|
together clips that don't belong together.
|
|
3. Trims the duplicated span off the start of each subsequent clip.
|
|
4. Re-encodes the video across the joins (trimming mid-GOP HEVC can't be done
|
|
with a plain stream copy) and concatenates everything into one file.
|
|
5. Carries over the original container metadata (GPS, device info, creation
|
|
time, Live Photo identifiers) from one of the source clips.
|
|
|
|
Audio stays uncompressed PCM throughout, so it never loses quality. Video
|
|
quality defaults to a bitrate slightly above the source clips' own bitrate
|
|
(configurable — see below).
|
|
|
|
## Requirements
|
|
|
|
- `ffmpeg` / `ffprobe` on `PATH`
|
|
- Python 3 with the packages in `requirements.txt`:
|
|
|
|
```bash
|
|
pip install --user -r requirements.txt
|
|
```
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
./concat_live_clips.py IMG_2441.MOV IMG_2442.MOV IMG_2443.MOV ...
|
|
./concat_live_clips.py -o myvideo.mov clip1.MOV clip2.MOV ...
|
|
```
|
|
|
|
If `-o/--output` is omitted, the output filename is derived from the source
|
|
filenames' numbering, e.g. `IMG_2441.MOV .. IMG_2445.MOV` produces
|
|
`IMG_2441-2445.mov`.
|
|
|
|
### Numbered sequence shortcuts
|
|
|
|
```bash
|
|
# bash / zsh
|
|
./concat_live_clips.py IMG_{2441..2445}.MOV
|
|
|
|
# PowerShell
|
|
python .\concat_live_clips.py (2441..2445 | ForEach-Object { "IMG_$_.MOV" })
|
|
```
|
|
|
|
### Options
|
|
|
|
| Flag | Description |
|
|
|---|---|
|
|
| `-o, --output` | Output file path (default: derived from source filenames) |
|
|
| `--crf N` | Use CRF (quality-based) encoding instead of the default bitrate target. Lower = higher quality; ~14-18 is near-transparent |
|
|
| `--bitrate RATE` | Explicit target video bitrate, e.g. `12M` or `12000k` |
|
|
| `--lossless` | Mathematically lossless video encoding (much larger output) |
|
|
| `--bitrate-multiplier N` | When auto-selecting a bitrate, multiply the source clips' peak bitrate by this (default: `1.2`) |
|
|
| `--bitrate-floor RATE` | When auto-selecting a bitrate, never go below this (default: `10M`) |
|
|
| `--preset` | x265 preset (default: `medium`) |
|
|
| `--metadata-from N` | Take container metadata from the Nth input clip, 0-indexed (default: `0`, the first clip) |
|
|
| `--confidence-threshold N` | Minimum audio cross-correlation confidence required to trust an overlap detection before falling back to 0 (default: `0.9`) |
|
|
| `--min-signal-rms N` | Below this audio RMS level (int16 scale), a boundary is considered too quiet to judge continuity, so a low-confidence match there won't trigger an abort (default: `25`) |
|
|
| `--allow-discontinuous` | Proceed even if audio at a clip boundary doesn't match, instead of aborting |
|
|
|
|
`--crf`, `--bitrate`, and `--lossless` are mutually exclusive; the default
|
|
(no flag) auto-computes a bitrate target from the source clips.
|
|
|
|
### Continuity check
|
|
|
|
Before encoding, each clip boundary's audio is checked for a real match.
|
|
Three outcomes:
|
|
|
|
- **Confident match** (confidence ≥ `--confidence-threshold`) — the overlap
|
|
is measured and trimmed as normal.
|
|
- **Too quiet to tell** (signal below `--min-signal-rms`) — treated as 0
|
|
overlap and the run proceeds, since there's no reliable signal either way.
|
|
- **Confident mismatch** (enough signal, but it doesn't correlate) — treated
|
|
as evidence the clips aren't actually a continuous sequence, and the run
|
|
aborts with an error before any encoding happens. Pass
|
|
`--allow-discontinuous` to concatenate them anyway.
|