# 按明确时间规则抽取一帧

本原创配方使用 Python 3 与已有 FFmpeg 在本机运行。实际记录环境是 Python 3.12.3、FFmpeg 6.0、macOS arm64。不需要 ffprobe、安装包、API 密钥、付费生成或模型结果。下载包里的短视频来自本站自行编写的 RGB 色块生成器，不是 H3 生成样片。

## 01. 确认工具，先建立真实帧表

将 extract-frame.py 和 run-fixtures.py 放在同一目录。FFmpeg 不在 PATH 时，设置已有可执行文件路径：

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

回执的 scriptHashes 绑定 extract-frame.py 和 run-fixtures.py：运行前计算两份脚本的 SHA-256，结束后再检查字节没有变化。引用已记录运行时，先核对下载脚本与回执哈希相同。

脚本拒绝覆盖已有输出目录，并自行建立短夹具，在 my-run/ 中保存回执、源视频、PNG、逐帧哈希和 CSV。下载包中的 executed/ 是已经执行的基线，无需覆盖。每个媒体进程只用一条线程，超时上限为 90 秒。在本项目的 16 GB Mac 上执行时，使用 codex-heavy 前缀；单独下载给其他用户的脚本不依赖这个项目包装器。

实际通过 16 个命名场景。CFR 有 10 帧，PTS 从 0 到 900，每步 100，time_base 为 1/1000；VFR 有 8 帧，使用同一时间基准：

| 零起解码帧索引 | PTS 整数 | 秒 |
|---:|---:|---:|
| 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 |

秒数 = PTS × time_base。不能用平均 FPS 推算 VFR 帧索引。脚本用 showinfo 枚举解码呈现顺序与时间基准，比较过程使用有理数，不靠四舍五入后的时间字符串判断。

## 02. 先选规则，不假设任意时间都恰好有一帧

命令里的时间相对第一帧解码 PTS，回执也包含非零起点测试。at 取目标之前或恰在目标上的最后一个帧起点；after 取目标之后或恰在目标上的第一个帧起点。它们都不承诺输出 PTS 等于任意输入时间。frame 按精确的零起解码帧索引选择，不是数据包编号或关键帧编号。

```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
```

第一条取帧 2，实际时间 0.12 秒；后两条取帧 3，实际时间 0.28 秒。目标恰好为 0.28 秒时，at 与 after 都取帧 3。CFR 目标 0.25 秒时，at 取帧 2 的 0.20 秒，after 取帧 3 的 0.30 秒。

本配方范围为第一帧到最后一帧的 PTS；超过最后 PTS 会停止，不推测尾帧应显示多久。缺失、重复或不递增 PTS，越界索引、已有输出和本地软链接输入也会停止。只接受本地文件，不抓取 URL。从起点完整解码，不使用 -ss，再用 select=eq(n\,INDEX) 按解码帧表位置选择。

## 03. 把像素验收与时间验收分开

每张 PNG 配有 .selection.json、.inventory.json、.framehash.txt。回执记录请求规则、原始 PTS、有理数时间基准、相对时间、源文件 SHA-256、PNG 文件 SHA-256、解码 RGB SHA-256 与尺寸。脚本会解码保存的 PNG，核对它的 RGB 像素与同一朝向策略下选中的源帧相同。

源帧 framehash 使用 -copyts、-fps_mode passthrough、原始 RGB 与明确编码时间基准。先看 #tb，才能解释整数 PTS。其数据包 duration 字段不用于证明播放器会保持一张 VFR 帧多久。像素一致比较解码 RGB 哈希；PNG 文件哈希标识编码文件，不同工具版本压缩出的 PNG 可以文件不同而像素相同。

## 04. 明确旋转策略，检查失败就停止

--orientation display 使用 FFmpeg 默认自动旋转；--orientation raw 在 -i 前传 -noautorotate。配套 90 度旋转元数据夹具实际得到 display 180×320、raw 320×180。输出 PNG 已把选定朝向写入像素网格，不假定视频旋转 side data 还会作为 PNG 的观看指令生效。

```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
```

这里实测一种 90 度元数据情况，没有验证所有显示矩阵、镜像变换、色彩管理链路、带 B 帧的编码或损坏时间戳。处理真实文件时，核对希望的朝向，并用该文件自身帧表对照回执。失败后保留输出查原因，不偷偷把 at 改成 after，不插帧，也不强制 CFR 来制造看似匹配的时间点。

## 官方资料

- [FFmpeg 命令参数](https://ffmpeg.org/ffmpeg.html)
- [select/showinfo/settb 滤镜](https://ffmpeg.org/ffmpeg-filters.html#select_002c-aselect)
- [framehash 输出格式](https://ffmpeg.org/ffmpeg-formats.html#framehash)

文档用于核对参数；实际回执只证明该夹具运行，不证明所有真实视频的任意时间点都存在精确的一帧。
