Summary
When a composition is sampled at a time t that falls between two frames of a <video> source, the snapshot shows the frame at or after t (ceil(t · videoFps)), not the frame whose interval contains t (floor(t · videoFps)). On-grid times are exact. Every off-grid time shows the next source frame, not only times whose fraction is ≥ 0.5.
Other renderers (After Effects, in our comparison) show the containing frame, so the two disagree by one source frame at every off-grid time. For a baked video plate, that shows up as content one frame early or late: in our case PSNR 21.77 against ≈ 99 at a nearby on-grid time.
Observed on hyperframes 0.8.92, macOS (arm64): hyperframes snapshot . --no-browser-gpu --at <t> --no-end.
Minimal reproduction (synthetic)
1. A 24 fps clip where each frame shows its own index. Frame N has background grey 5·N for machine readout, plus its index in large digits.
# make_frames.py — 48 frames at 24 fps (2 s)
from PIL import Image, ImageDraw, ImageFont
import os
os.makedirs("frames", exist_ok=True)
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 200)
for n in range(48):
im = Image.new("RGB", (640, 360), (n * 5,) * 3)
d = ImageDraw.Draw(im); t = str(n); w, h = d.textbbox((0, 0), t, font=font)[2:]
d.text(((640 - w) / 2, (360 - h) / 2 - 20), t, fill=(255, 255, 255), font=font)
im.save(f"frames/f{n:03d}.png")
python3 make_frames.py
ffmpeg -framerate 24 -i frames/f%03d.png -c:v libvpx-vp9 -pix_fmt yuv420p -b:v 1M frames24.webm
(If your ffmpeg has freetype, drawtext=text='%{frame_num}' over a color source works too.)
2. A one-element composition (index.html):
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>video frame selection repro</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>html,body{margin:0;padding:0;background:#000}</style>
</head>
<body>
<div id="root" data-composition-id="main" data-width="640" data-height="360" data-duration="2" style="position:relative;overflow:hidden;width:640px;height:360px;background:#000">
<video id="v" class="clip" data-start="0" data-duration="2" data-track-index="1" src="frames24.webm" muted style="position:absolute;left:0;top:0;width:640px;height:360px"></video>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["main"] = gsap.timeline({ paused: true });
</script>
</body>
</html>
3. Snapshot at times on the 1/30 s grid that fall between 24 fps frames. Pass full-precision times: 0.7333 would floor to the previous 1/30 frame, see #4430.
hyperframes snapshot . --no-browser-gpu --at 0.4,0.7333333333333333,0.3333333333333333 --no-end
Results
--at |
source frame position |
expected (containing frame) |
observed |
| 12/30 = 0.4 |
9.6 |
9 |
10 |
| 22/30 = 0.7333… |
17.6 |
17 |
18 |
| 10/30 = 0.3333… (control, on both grids) |
8.0 |
8 |
8 |
The digits and the machine-read background grey agree (50, 90, 40).
Related
#4430 (snapshot --at renders on a 30 fps grid) is separate: the times above are already on that grid, and the frame chosen for them is still the next one.
Ask
Select the source frame as floor(t · videoFps + ε), the frame whose interval contains t. Or document the rule, so callers can align their sample times.
Summary
When a composition is sampled at a time
tthat falls between two frames of a<video>source, the snapshot shows the frame at or aftert(ceil(t · videoFps)), not the frame whose interval containst(floor(t · videoFps)). On-grid times are exact. Every off-grid time shows the next source frame, not only times whose fraction is ≥ 0.5.Other renderers (After Effects, in our comparison) show the containing frame, so the two disagree by one source frame at every off-grid time. For a baked video plate, that shows up as content one frame early or late: in our case PSNR 21.77 against ≈ 99 at a nearby on-grid time.
Observed on hyperframes 0.8.92, macOS (arm64):
hyperframes snapshot . --no-browser-gpu --at <t> --no-end.Minimal reproduction (synthetic)
1. A 24 fps clip where each frame shows its own index. Frame N has background grey
5·Nfor machine readout, plus its index in large digits.(If your ffmpeg has freetype,
drawtext=text='%{frame_num}'over acolorsource works too.)2. A one-element composition (
index.html):3. Snapshot at times on the 1/30 s grid that fall between 24 fps frames. Pass full-precision times:
0.7333would floor to the previous 1/30 frame, see #4430.hyperframes snapshot . --no-browser-gpu --at 0.4,0.7333333333333333,0.3333333333333333 --no-endResults
--atThe digits and the machine-read background grey agree (50, 90, 40).
Related
#4430 (
snapshot --atrenders on a 30 fps grid) is separate: the times above are already on that grid, and the frame chosen for them is still the next one.Ask
Select the source frame as
floor(t · videoFps + ε), the frame whose interval containst. Or document the rule, so callers can align their sample times.