Automated release: version and notes generated from pull requests merged since 3.8.15.
Fixed
- A variable-rate source is conformed by the
fpsfilter, and its picture stays with its sound
(#512). The re-encoding tools that conform a VFR source throughcfr_args(broll,
caption,color,crop,cut,deinterlace,denoise,export,fit,freeze,
graphics,loop,overlay,pad,proxy,redact,sphere,stabilize,straighten) left the conform to the encoder line's-fps_mode cfr -r, which let a frame fall
up to about two frames behind its slot before it dropped one: on a 30 fps clip with frames
dropped to an average of 27 fps, flashes came out 18 and 56 ms after their beeps and the picture
ran 74 ms longer than the source's. Thefpsfilter now takes the nearest frame for each slot,
and on that clip each of those tools wrote every flash 18-19 ms after its beep, within half a
frame (FFmpeg 6.1.1).speedramp.pyandfreeze.pyinside the clip closed their graph on the
source's nominal rate and let-rresample it (speedramp.py's flash after a slowed segment
came 74 ms late, 114 ms against the sound); they close on the delivered rate.freeze.pycounted a VFR hold in average-rate frames spaced at the nominal rate (0.9 s of
picture against 1 s of sound for--hold 1).fit.py --method speedputs its retimed picture
back on the grid the same way (a 1.11x speed-up of a constant-rate clip showed each flash 50 ms
late); with--fpsit goes straight to the rate asked (a 2x speed-up of a 30 fps clip to
--fps 60went throughfps=30, keeping 60 of its 120 pictures, thenfps=60). The rate
comes first in a-vfthat is a split/overlay or maskedmerge graph (fit.py --fit blurand
--pad-fill blur,export.py's pad with--pad-fill bluror a--pad-colorwith chroma),
after any retiming: FFmpeg 6.1's overlay ends at its last frame's start, and anfpsfilter
after it lost that frame (export.py --preset reels --pad-fill blurwrote 119 frames of a 4 s
30 fps picture,fit.py --fit blur --fps 60238 of 240,--fit bluron the 27 fps clip 107
of 108).speedramp.pygives each segment the frames between its retimed ends on the whole
output's grid and joins the picture and the sound by separate concats: each segment's own
length rounded to the grid drifted, eight 0.5 s segments at 1x of the 27 fps clip ran 4.148 s
and put the last flash, and its beep, at 3.630 s for 3.5. The joined picture's timestamps are
then rebuilt from its frame count: FFmpeg 6.1.1's video-only concat gave a one-frame segment
no length, and three of them in a row (0-0.1:3,0.1-0.2:3,0.2-0.3:3on a 30 fps clip)
put the rest of the picture a frame before its sound. Not changed:speedramp.pyputs
its sound 21-40 ms early (atempo, also on a constant rate), afreeze.py --atinside a VFR
gap writes the picture after the hold early, andreverse.pystill shows a reversed VFR
picture late (0.19 s on that clip); the three are filed as #523. overlay.pyplaces an--imageor--videolayer at the pixel its position names (#512).
overlayputs a layer on a 4:2:0 picture on the chroma grid, so--position 11,13drew the
logo at 10,12, and a right-aligned, bottom-aligned or centred layer whose position came out odd
moved one pixel left or up. The layer gets a transparent column or row where its position is
odd and is overlaid at the even one before it, so its luma lands where asked; the chroma sample
on that edge is shared with the pixel outside it. A dry run whose input does not exist yet
plans the old graph (no size to compute the position from).--textwas already exact. An
--imagethat stores non-square pixels (a PNG's pHYs) is made square at its display aspect
first, as a--videolayer is: its position was computed from its displayed size while it
was drawn at its stored one (a 100x100 PNG at SAR 2:1 drew 100 px wide at x=120 of a 320 px
main for--position top-right --margin 0).fit.py --fit cropkeeps the edge or the centre--crop-x/--crop-ynames (#512).crop
floored the computed offset to the chroma grid, so over an odd margin--crop-x 1kept a column
of the margin and lost the source's last one, and a centred crop sat a pixel off. The scaled
frame's margin is now a multiple of 4 (at most 3 px wider on the cropped axis) and the offset
is computed to even, asstraighten.py --fit cropdoes. An anchor whose offset is not even
(--crop-x 0.25over a 36 px margin asks for 9 px) gets the even one nearest it, and a
notesline names the axis, the offset asked and the one applied.render.py --export-timelinesizes a one-clip sequence by the project'sfitobject
(#506). The render's fit stage reframes with thefitobject's aspect, mode, sides and rate
and takes the frame's only where it has none; the export read the frame alone, so frame 16:9
withfit1:1 rendered a square and exported a 16:9 sequence. Afit.aspectorfit.fit
that fit.py refuses is refused by the export under its own name. A project of several clips
is unchanged (its join is sized from the frame first; which offrameandfitwins there is
deferred, seedocs/design-decisions.md).- fix: a VFR conform keeps the picture with its sound; odd overlay and crop positions land where asked; the timeline takes the fit object's frame (#525)
What's Changed
- fix: a VFR conform keeps the picture with its sound; odd overlay and crop positions land where asked; the timeline takes the fit object's frame by @kajisho5 in #525
Full Changelog: v3.8.15...v3.8.16