Automated release: version and notes generated from pull requests merged since 3.1.5.
Fixed
- On FFmpeg 7.0+,
freeze.py --atlost its hold (--at 1 --hold 1on a 2 s clip came out 1.97 s, and
the sound ran a second past the picture) andreverse.pydid not hold the last frame of a picture
shorter than its sound (408 ms off): from 7.0setptsleaves the frame rate unknown and the frame
durations 0, so atpadclone after it adds nothing. Both now clone before the shift; the graph
is the same on every version, and both end on anfpsat the source's rate, which gives back the
frame durationssetptsclears (the last frame was written with none: 3.967 s for 4 s) (#409, #411). color.py --lutshifted every colour even with an identity LUT (2FAE4E came out 249A4B): libavfilter's
own conversion intolut3d's RGB read the frame's BT.709 tag and the one back used BT.601. Both legs
are now explicit BT.709 conversions through float RGB, and a--lut-strengthblend is made in that
RGB too; an identity LUT returns what--lut-strength 0does (#410).join.py,multicam.py,grid.py,broll.pyandoverlay.py --videomixed a full-range clip in a
plain pixel format (VP9 and AV1 full range, any 10-bit full range, which decode asyuv420ptagged
pc) with limited-range ones without converting the range, so one of them was shown in the other's
range (C03028 came out C9251B). Every SDR picture these tools mix is now taken to limited range
first. The limited-range layersgraphics.py(boxes and bars with a colour), emoji PNGs and
overlay.py --imagecomposite were put on a full-range main the same way (30B050 came out 37A757);
that main is now taken to limited range under them (#410).- On FFmpeg before 7.1, a colour drawn on such a full-range picture landed in limited range: a pad's
black read 101010 and white EBEBEB (pad.py,fit.py,overlay.py --text...), and a colour with
chroma on a 10-bit full-range picture came out oversaturated. The picture is now taken to limited
range before the drawing on those versions; FFmpeg 7.1+ commands are unchanged (#410). look.py(frames,--compare, contact sheet),scenes.py --sheetandexport.py --preset gif
decoded an untagged source as BT.601 (C03028 came out B31F28), while every encode takes it as
BT.709. Its frames are now labelled BT.709 (tags only) before the conversion to RGB (#410).color.py --correctwith every option at its default tinted grey (808080 came out 7E7E7B):
colortemperature=temperature=6500was always in the chain and is not an identity. The term is
left out at 6500 K (#410).grid.py --cols 1 --rows 1reached ffmpeg and failed there with exit 222 (xstacktakes two inputs
or more). It is now refused up front as an input error (#410).silence.pydrifted picture and sound apart at every cut, even on a clean source: each kept range
was rounded twice, the picture to whole frames and the sound to whole AAC frames, and the errors
added up (about 150 ms over 21 ranges of a 61 s talk; 5, 40 and 75 ms over three ranges), and
--timemapwas off by as much. A sound that starts after its picture was also moved early by its
whole offset (+280 ms). The kept ranges are now re-cut the waycut.py --segments --accurate
cuts them: both streams on the same frame-grid bounds, shifted by one constant, a late sound padded
with silence. Each kept bound is moved to the nearest frame time first, and--timemapis built from
those bounds, so its output times are the output's. The old render kept both boundary frames of
every range, one frame more per range than was kept (#409).sync.pyandmulticam.pymeasured a sound track that starts after its file's start from its
first sample: a track 0.3 s late measured +2.279 s for +2.000 s,--replace-audioplaced it about
270 ms late, andmulticam.pycut the camera's picture that far off and rebased its sound too. The
audio decode now pads a late sound so its samples are on the file's clock (this also corrects
scenes.py's audio envelopes andcut.py's beat grid on such files), and both tools place the
sound on its own file's clock before delaying it (#409).freeze.py,speedramp.py,join.py(since #338),broll.py --audio b|mixandreverse.pymoved
one stream against the other on a source whose sound or picture starts late, or whose sound runs
past its picture:freeze+279 ms (late sound) and -100 ms (late picture),speedramp+570 ms at
0.5x,joinandbroll+279 ms,reverse+396 ms for a sound 0.6 s past the picture (a
stream-copied cut went from -17 to +61 ms). Each now cuts both streams on the same frame times and
shifts them by one constant, ascut.py's re-cut does;reverse.pyturns both about one end (#409).loop.pylost a frame and slid the sound on every repeat of a clip whose sound starts late (795 of
810 frames, 43-51 ms):-stream_looptook each repeat's length from the streams' spans without the
last frame. The repeats now come from the concat demuxer, one entry per repeat, each the file's
whole length, with the late sound's gap filled with silence (#409).- Footage that stores non-square pixels (DV, HDV/XDCAM 1440x1080, DVD) came out squashed from
every tool that scales (fit, export, grid, join, broll, multicam, look, ...),crop.pyread its
rectangle in the stored frame, and caption,overlay.py --textand graphics drew text the
player stretched 1.78x.probe.pynow reportssample_aspect_ratio, and every tool that sizes,
crops, places or draws on the picture first scales it to square pixels at its display aspect
(its height kept) and says so innotes; rectangles, margins and sizes are in that displayed
frame. A tool that only re-encodes keeps the stored size and its SAR (#411). graphics.py'stitle,lower-third,chapterandbugnever wrapped, so a long title ran
off the frame (also throughrender.py --template). They now wrap with the samewrapped()
helpersticker,hookandmemeuse, and the lower-third bar widens to the widest line.
overlay.py --textwraps a line wider than the frame between the margins and says so innotes
(#411).look.py's contact sheet stamped an MPEG-TS's tiles one seek late (about +0.5 s); it now reads
the stamps off the file's own timestamps.scenes.py --sheetpicked the wrong frames on a
variable-frame-rate source (a frame number from start x average fps); it now picks each scene's
first frame by time.look.py --comparestretched the first picture to the second's aspect; both
are now letterboxed into boxes as tall as the taller one (#411).grid.pydrew a Japanese, Chinese or Thai filename label as empty boxes while reporting
substituted: false, and a label wider than its cell kept only its tail. Each label now gets a
font that covers its script, and a long one is shortened from the end with an ellipsis (#411).freeze.py --at Theld the frame before T (at a cut, the last frame of the previous shot); it
now holds the frame shown at T (#411).- An hh:mm:ss:ff time typed off a source's start timecode (01:00:01:00 on a camera file starting at
01:00:00:00) was refused as past the end with no word on why. The refusal'shintnow says the
time counts from the file's first frame and names the frame meant; the time grammar is unchanged
(#411). broll.pywith an odd-sized A-roll failed unclassified (kind: ffmpeg); the A-roll is scaled to
even up front and recorded asodd_dimensions_scaled_even(#411).- A JPEG with an EXIF Orientation tag (a phone's portrait photo) was sized as landscape by
insert.pyandsequence.py, which cropped a sideways window out of the upright picture
ffmpeg decodes.probe()now reads a still's rotation off its decoded frame (#411). - Review of #416:
freeze.pywithout--at(the clip's end) held nothing in--mode insert(the
picture kept its length, the sound gained the hold); it now holds the last frame as--mode extend
does. On a variable frame rate a hold whose one-frame window held no frame (frames at 0.033 and
0.100,--at 0.06) was lost too; the hold is now the first frame at or after the cut (#409, #411). silence.pywith more than 32 kept ranges replaced its input under-o <input> --overwrite: the
chunked join's last command never names the source, so the same-file check did not see it. It is
now refused up front. Itskeep,kept_duration,removed_secondsand--edldescribed the
unsnapped bounds while the render and--timemapheld frame times (0.385-2.115 for 0.4-2.1), and
an EDL bound rounded past its frame re-cut one frame late; all now describe the rendered ranges,
frame times written down to the millisecond (#409).insert.pynoted the square-pixel scale of a non-square-pixel still but never ran it, so the still
lost a band top and bottom;cropdetect.py,sequence.pyandwaveform.py --imagesquared the
picture without thenotesline. The EXIF-rotation probe read a still namedphoto%02d.jpgas an
image sequence and found no rotation (#411).straighten.py --fit croppre-scaled by a square's factor (|cos|+|sin|) and left two corners black
on a 16:9 frame; it now scales by |cos| + max(W/H, H/W)|sin| of the displayed frame (#411).speedramp.pyaccepted a segment whose two ends fall on one frame (1.01-1.02 at 30 fps) and
applied its factor to no frame; it is now refused as an input error (#409).graphics.pyplaced a PNG emoji on a wrapped line as if the label were one line (first line's y,
the earlier lines' width added to x); a title wrapped taller than the frame, andoverlay.py --text
wrapped taller than the frame or placed too near the edge to wrap, lost text without a word (now a
notesline); the lower-third bar's 62 % floor ran past the right margin at a large--scale; a
grid label in a cell with room for one em was not shortened;broll.pyscaled a 1-pixel-wide
A-roll to width 0 (kept as 1) and failed; and the SMPTE hint named the first timecoded input's
start timecode for a time in another input (a B-roll's--from) (#411).- Colour, A/V sync and geometry: the round-1 audit fixes (#410, #409, #411) (#416)
What's Changed
Full Changelog: v3.1.5...v3.1.6