# Extract one frame with an explicit timestamp policy

This original recipe runs locally with Python 3 and an existing FFmpeg installation.
Actual baseline: Python 3.12.3, FFmpeg 6.0, macOS arm64. No ffprobe, package installation, API key, paid generation or model result is involved. The downloads contain tiny synthetic fixtures made by our original RGB-tile generator; they are not H3 video samples.

## 01. Install no package; identify the tool and inventory

Place extract-frame.py and run-fixtures.py in one directory. If FFmpeg is not on PATH, set its installed absolute path:

```sh
export FFMPEG_BIN=/YOUR/INSTALLED/ffmpeg
"$FFMPEG_BIN" -version
python3 run-fixtures.py --out-dir ./my-run
```

The receipt binds extract-frame.py and run-fixtures.py through scriptHashes: both SHA-256 values are captured before execution and rechecked afterward. Compare them with the downloaded files before relying on the recorded run.

The fixture runner refuses an existing output directory and creates its own short media. It writes an execution receipt, original fixtures, PNGs, complete framehash logs and a CSV table into my-run/. Existing executed/ is the recorded baseline and need not be overwritten. Every media subprocess is single-threaded and has a 90-second timeout. On this project's 16 GB Mac, run the command through codex-heavy; the standalone download does not require that project-specific wrapper.

The runner verifies 16 named scenarios. CFR uses ten frames, PTS 0–900 by 100, and time_base 1/1000. VFR uses eight frames and the same time base:

| Zero-based decoded index | PTS ticks | Seconds |
|---:|---:|---:|
| 0 | 0 | 0.00 |
| 1 | 40 | 0.04 |
| 2 | 120 | 0.12 |
| 3 | 280 | 0.28 |
| 4 | 320 | 0.32 |
| 5 | 600 | 0.60 |
| 6 | 640 | 0.64 |
| 7 | 920 | 0.92 |

Seconds = PTS × time_base. Do not calculate VFR index from average FPS. showinfo inventories decoded presentation order and its time base; the script keeps rational arithmetic rather than rounding timestamp strings.

## 02. Choose the rule, not an imaginary exact timestamp

All CLI times are relative to the first decoded presentation timestamp. Nonzero-start fixture validation is included. `at` means the latest decoded frame start at or before the target; `after` means the first frame start at or after it. Neither promises a frame whose PTS equals an arbitrary requested number. `frame` chooses an exact zero-based decoded index, not a packet number or keyframe number.

```sh
python3 extract-frame.py --input executed/vfr.mkv --rule at --time 0.20 --out ./at.png
python3 extract-frame.py --input executed/vfr.mkv --rule after --time 0.20 --out ./after.png
python3 extract-frame.py --input executed/vfr.mkv --rule frame --index 3 --out ./index3.png
```

The first command selects index 2 at 0.12 s. The next two select index 3 at 0.28 s. When the target is exactly 0.28 s, at and after both select index 3. For CFR, target 0.25 s selects index 2 at 0.20 s for at and index 3 at 0.30 s for after.

The defined interval is the first-to-last decoded PTS range. The script rejects targets beyond the last PTS because the final frame's display duration is not inferred. It also refuses missing/repeated/non-monotonic timestamps, out-of-range indices, existing outputs and local symlink inputs. Inputs are local files; there is no URL fetching. It decodes from the beginning without -ss and selects with select=eq(n\,INDEX); the frame's identity is therefore the decoded inventory position.

## 03. Check the pixels and time base independently

Each PNG gets `.selection.json`, `.inventory.json` and `.framehash.txt` sidecars. The receipt records the requested rule, original PTS, rational time base, relative timestamp, input file SHA-256, PNG file SHA-256, decoded RGB SHA-256 and dimensions. The script decodes the saved PNG and verifies that its RGB pixels match the selected source frame under the same orientation choice.

The source framehash command uses -copyts, -fps_mode passthrough, raw RGB and an explicit encoder time base. Read #tb before interpreting its integer PTS. Its packet duration field is not used as proof of how long a viewer holds a VFR frame. Compare decoded RGB hashes for pixel identity; PNG file hashes identify encoded files and can differ across encoder/tool versions even when pixels match.

## 04. Decide the rotation policy and stop at a failed check

`--orientation display` follows FFmpeg's default autorotation; `--orientation raw` supplies -noautorotate before -i. The provided 90-degree metadata fixture extracts 180×320 display pixels and 320×180 raw pixels. PNG has the selected orientation baked into its pixel grid; do not assume video rotation side data is preserved as a meaningful PNG viewing instruction.

```sh
python3 extract-frame.py --input executed/rotation-90.mp4 --rule frame --index 0 --orientation display --out ./display.png
python3 extract-frame.py --input executed/rotation-90.mp4 --rule frame --index 0 --orientation raw --out ./raw.png
```

The fixture verifies one 90-degree metadata case, not every display matrix, mirrored transform, color-management pipeline, B-frame codec or damaged timestamp stream. For a real file, confirm its desired orientation and compare the receipt to its own decoded inventory. Preserve the failed run and investigate; do not silently change at to after, generate intermediate frames or force CFR to manufacture a match.

## Official references

- [FFmpeg command-line options](https://ffmpeg.org/ffmpeg.html)
- [select/showinfo/settb filters](https://ffmpeg.org/ffmpeg-filters.html#select_002c-aselect)
- [framehash muxer](https://ffmpeg.org/ffmpeg-formats.html#framehash)

The documentation explains the flags. The attached actual receipt proves this fixture run, and does not establish that every real input can supply an exact frame at every timestamp.
